Skip to content

Session 2: Managing State in Python Applications

Synopsis

Covers how to represent and store session state, task progress, memory summaries, and interaction logs. Learners develop the application engineering mindset needed to make agentic systems reproducible and debuggable.

Session Content

Session 2: Managing State in Python Applications

Session Overview

Duration: ~45 minutes
Audience: Python developers with basic programming knowledge, beginning their GenAI and agentic development journey

Learning Goals

By the end of this session, learners will be able to:

  • Explain what "state" means in Python applications
  • Distinguish between stateless and stateful program design
  • Manage state using variables, functions, classes, and dictionaries
  • Persist state across interactions in simple GenAI applications
  • Build a small Python chatbot loop that maintains conversational context
  • Use the OpenAI Responses API with gpt-5.4-mini in a state-aware application

Agenda

  1. Introduction to State in Applications
  2. Stateless vs Stateful Design
  3. Common Patterns for Managing State in Python
  4. State in GenAI Applications
  5. Hands-on Exercise 1: Tracking State in Plain Python
  6. Hands-on Exercise 2: Building a Stateful Chat Loop with OpenAI
  7. Wrap-up and Key Takeaways

1. Introduction to State in Applications

What is State?

State is the data an application remembers at a given moment.

Examples:

  • A shopping cart's contents
  • A logged-in user's name
  • A chatbot's prior conversation
  • A counter's current value
  • A workflow's current step

In Python, state often lives in:

  • Variables
  • Lists and dictionaries
  • Class instances
  • Files or databases
  • In-memory caches

Why State Matters

Without state, many useful applications would not work well:

  • A chatbot would forget what you just said
  • A game would reset every move
  • A form would forget entered data
  • An agent would lose track of task progress

For GenAI systems, state is especially important because it helps maintain:

  • Conversation context
  • User preferences
  • Tool outputs
  • Intermediate reasoning results
  • Execution history

2. Stateless vs Stateful Design

Stateless Programs

A stateless program does not remember prior interactions unless all required input is passed in each time.

Example

def greet(name: str) -> str:
    return f"Hello, {name}!"

This function does not remember anything from previous calls.

Stateful Programs

A stateful program stores information that affects future behavior.

Example

class Greeter:
    def __init__(self):
        self.count = 0

    def greet(self, name: str) -> str:
        self.count += 1
        return f"Greeting #{self.count}: Hello, {name}!"

This class remembers how many times it has greeted someone.

Comparing the Two

Aspect Stateless Stateful
Memory of prior actions No Yes
Simplicity Usually simpler Often more complex
Reproducibility Easier Can be harder
Use cases Pure functions, utilities Chats, workflows, sessions

Design Insight

Stateless systems are often easier to test and scale.
Stateful systems are often more natural for real-world interactive applications.

A good engineer knows when to use each.


3. Common Patterns for Managing State in Python

3.1 State with Variables

The simplest form of state is a variable whose value changes over time.

counter = 0

counter += 1
counter += 1

print(counter)

Example output:

2

This works for small scripts, but can become hard to manage in larger applications.


3.2 State with Dictionaries

Dictionaries are great for storing flexible application state.

app_state = {
    "user_name": "Ava",
    "logged_in": True,
    "messages_sent": 3,
}

print(app_state["user_name"])
print(app_state["messages_sent"])

Example output:

Ava
3

Why dictionaries are useful

  • Easy to inspect
  • Flexible structure
  • Good for JSON serialization
  • Common in API-driven applications

3.3 State with Functions and Parameters

A cleaner alternative to global state is passing state into functions and returning updated state.

def increment_counter(state: dict) -> dict:
    new_state = state.copy()
    new_state["counter"] += 1
    return new_state

state = {"counter": 0}
state = increment_counter(state)
state = increment_counter(state)

print(state)

Example output:

{'counter': 2}

This pattern is helpful because it reduces hidden side effects.


3.4 State with Classes

Classes allow related data and behavior to live together.

class SessionState:
    def __init__(self, user_name: str):
        self.user_name = user_name
        self.message_count = 0

    def record_message(self) -> None:
        self.message_count += 1

state = SessionState("Ava")
state.record_message()
state.record_message()

print(state.user_name)
print(state.message_count)

Example output:

Ava
2

Why classes help

  • Group related logic
  • Support encapsulation
  • Useful for long-running sessions or agents

3.5 Persisting State to a File

In many applications, state must survive program restarts.

import json

state = {
    "user_name": "Ava",
    "message_count": 5
}

with open("state.json", "w", encoding="utf-8") as f:
    json.dump(state, f, indent=2)

with open("state.json", "r", encoding="utf-8") as f:
    loaded_state = json.load(f)

print(loaded_state)

Example output:

{'user_name': 'Ava', 'message_count': 5}

Common persistence options

  • JSON files
  • SQLite
  • Redis
  • PostgreSQL
  • Cloud storage

For learning purposes, JSON is a great starting point.


4. State in GenAI Applications

State is one of the core building blocks of GenAI applications.

Examples of GenAI State

  • Prior user messages
  • Prior assistant responses
  • System instructions
  • Retrieved documents
  • Tool outputs
  • User preferences
  • Temporary workflow memory

Why Chat Applications Need State

If the model only sees the latest user message, it loses context.

Without conversation state

User: "My name is Maya."
User: "What is my name?"

If only the second message is sent, the model cannot know the answer.

With conversation state

The app stores earlier messages and includes them in future requests.

Important Design Principle

The model itself does not automatically "remember" your local application's conversation history unless you provide or reference that state in your API usage.

Your Python application is responsible for managing it.


5. Hands-on Exercise 1: Tracking State in Plain Python

Objective

Build a small session tracker that stores:

  • User name
  • Number of actions
  • Recent events

Concepts Practiced

  • Dictionaries
  • Updating state
  • Keeping recent history
  • Displaying current state

Starter Code

def create_state(user_name: str) -> dict:
    """
    Create a new application state dictionary.

    Args:
        user_name: The name of the current user.

    Returns:
        A dictionary representing session state.
    """
    return {
        "user_name": user_name,
        "action_count": 0,
        "recent_events": []
    }


def record_event(state: dict, event: str, max_events: int = 5) -> dict:
    """
    Return an updated copy of state with a new event recorded.

    Args:
        state: Existing application state.
        event: Description of the event to record.
        max_events: Maximum number of recent events to keep.

    Returns:
        A new state dictionary.
    """
    new_state = state.copy()
    new_state["action_count"] += 1

    # Copy the list so we do not mutate the original state in place.
    events = list(new_state["recent_events"])
    events.append(event)

    # Keep only the most recent N events.
    new_state["recent_events"] = events[-max_events:]
    return new_state


def print_state(state: dict) -> None:
    """
    Print the current session state in a readable format.
    """
    print("\n--- CURRENT STATE ---")
    print(f"User: {state['user_name']}")
    print(f"Action count: {state['action_count']}")
    print("Recent events:")
    for item in state["recent_events"]:
        print(f" - {item}")


def main() -> None:
    """
    Demonstrate simple state management.
    """
    state = create_state("Maya")
    state = record_event(state, "Opened application")
    state = record_event(state, "Viewed dashboard")
    state = record_event(state, "Clicked help")
    print_state(state)


if __name__ == "__main__":
    main()

Example Output

--- CURRENT STATE ---
User: Maya
Action count: 3
Recent events:
 - Opened application
 - Viewed dashboard
 - Clicked help

Exercise Tasks

  1. Add a "theme" field to the state
  2. Add a function to change the theme
  3. Add a timestamp string to each event
  4. Limit recent events to the last 3 items
  5. Print the state after each update

Challenge Extension

Modify the event list so each event is stored as a dictionary like:

{"timestamp": "2026-03-22 10:00:00", "event": "Opened application"}

6. Hands-on Exercise 2: Building a Stateful Chat Loop with OpenAI

Objective

Create a simple command-line chatbot that remembers previous messages using Python-managed state and the OpenAI Responses API.

What You Will Build

A Python program that:

  • Stores conversation history in memory
  • Sends user input to gpt-5.4-mini
  • Includes prior messages in future turns
  • Exits when the user types quit

Prerequisites

Install the OpenAI Python SDK:

pip install openai

Set your API key:

export OPENAI_API_KEY="your_api_key_here"

On Windows PowerShell:

$env:OPENAI_API_KEY="your_api_key_here"

6.1 Core Idea

We will maintain conversation state in a Python list.
Each new user turn is appended to the list, then sent to the Responses API.


6.2 Full Example: In-Memory Stateful Chat

from openai import OpenAI

# Create a client using the API key from the OPENAI_API_KEY environment variable.
client = OpenAI()


def create_chat_state(system_prompt: str) -> dict:
    """
    Create initial chatbot state.

    Args:
        system_prompt: High-level instruction for the assistant.

    Returns:
        A dictionary containing the conversation state.
    """
    return {
        "system_prompt": system_prompt,
        "history": []
    }


def add_user_message(state: dict, message: str) -> None:
    """
    Append a user message to conversation history.

    Args:
        state: The chatbot state dictionary.
        message: The user's text input.
    """
    state["history"].append({
        "role": "user",
        "content": message
    })


def add_assistant_message(state: dict, message: str) -> None:
    """
    Append an assistant message to conversation history.

    Args:
        state: The chatbot state dictionary.
        message: The assistant's response text.
    """
    state["history"].append({
        "role": "assistant",
        "content": message
    })


def build_input_messages(state: dict) -> list:
    """
    Convert internal state into the input format for the Responses API.

    Args:
        state: The chatbot state dictionary.

    Returns:
        A list of message objects.
    """
    messages = [
        {
            "role": "system",
            "content": state["system_prompt"]
        }
    ]

    for item in state["history"]:
        messages.append({
            "role": item["role"],
            "content": item["content"]
        })

    return messages


def get_model_response(state: dict) -> str:
    """
    Send the current conversation to the model and return the assistant's reply.

    Args:
        state: The chatbot state dictionary.

    Returns:
        The assistant response text.
    """
    response = client.responses.create(
        model="gpt-5.4-mini",
        input=build_input_messages(state)
    )

    return response.output_text


def main() -> None:
    """
    Run a simple stateful command-line chat application.
    """
    state = create_chat_state(
        system_prompt=(
            "You are a helpful Python tutor. "
            "Answer clearly and briefly. "
            "Remember details from the conversation."
        )
    )

    print("Stateful Chatbot")
    print("Type 'quit' to exit.\n")

    while True:
        user_input = input("You: ").strip()

        if user_input.lower() == "quit":
            print("Goodbye!")
            break

        if not user_input:
            print("Please enter a message.")
            continue

        add_user_message(state, user_input)
        assistant_reply = get_model_response(state)
        add_assistant_message(state, assistant_reply)

        print(f"Assistant: {assistant_reply}\n")


if __name__ == "__main__":
    main()

Example Usage

Stateful Chatbot
Type 'quit' to exit.

You: My name is Maya.
Assistant: Nice to meet you, Maya! How can I help you with Python today?

You: What is my name?
Assistant: Your name is Maya.

You: quit
Goodbye!

6.3 Why This Works

The program keeps a history list in Python memory.

Each request includes:

  • The system prompt
  • All prior user messages
  • All prior assistant messages

That means the model receives enough context to answer follow-up questions.


6.4 Improvement: Save and Load Chat State from JSON

This version persists conversation state so you can restart the app without losing history.

import json
from pathlib import Path
from openai import OpenAI

client = OpenAI()
STATE_FILE = Path("chat_state.json")


def create_chat_state(system_prompt: str) -> dict:
    """
    Create a new chat state object.
    """
    return {
        "system_prompt": system_prompt,
        "history": []
    }


def load_state() -> dict:
    """
    Load state from disk if available; otherwise create a fresh state.

    Returns:
        A chat state dictionary.
    """
    if STATE_FILE.exists():
        with STATE_FILE.open("r", encoding="utf-8") as f:
            return json.load(f)

    return create_chat_state(
        system_prompt=(
            "You are a helpful Python tutor. "
            "Be concise, friendly, and context-aware."
        )
    )


def save_state(state: dict) -> None:
    """
    Save state to disk as JSON.

    Args:
        state: The chat state dictionary.
    """
    with STATE_FILE.open("w", encoding="utf-8") as f:
        json.dump(state, f, indent=2, ensure_ascii=False)


def build_input_messages(state: dict) -> list:
    """
    Convert chat state into a Responses API input list.
    """
    messages = [
        {
            "role": "system",
            "content": state["system_prompt"]
        }
    ]
    messages.extend(state["history"])
    return messages


def main() -> None:
    """
    Run a persistent, stateful chat loop.
    """
    state = load_state()

    print("Persistent Stateful Chatbot")
    print("Type 'quit' to exit.")
    print("Type 'reset' to clear conversation history.\n")

    while True:
        user_input = input("You: ").strip()

        if user_input.lower() == "quit":
            save_state(state)
            print("State saved. Goodbye!")
            break

        if user_input.lower() == "reset":
            state["history"] = []
            save_state(state)
            print("Conversation history cleared.\n")
            continue

        if not user_input:
            print("Please enter a message.\n")
            continue

        state["history"].append({
            "role": "user",
            "content": user_input
        })

        response = client.responses.create(
            model="gpt-5.4-mini",
            input=build_input_messages(state)
        )

        assistant_text = response.output_text

        state["history"].append({
            "role": "assistant",
            "content": assistant_text
        })

        save_state(state)
        print(f"Assistant: {assistant_text}\n")


if __name__ == "__main__":
    main()

Example Output

Persistent Stateful Chatbot
Type 'quit' to exit.
Type 'reset' to clear conversation history.

You: I am learning about Python classes.
Assistant: Great topic—Python classes help you bundle data and behavior together.

You: What was I learning about?
Assistant: You were learning about Python classes.

You: quit
State saved. Goodbye!

6.5 Best Practices for State in GenAI Apps

Keep state explicit

Avoid hidden global state when possible.

Store only what you need

Too much history can increase cost and latency.

Persist important state

If the app must recover after a restart, save it.

Validate state shape

Use consistent field names and expected formats.

Separate app state from model calls

This makes your code easier to test and maintain.

Consider trimming conversation history

In larger applications, retain only relevant recent messages or summaries.


7. Mini Lab: Add Structured Memory to the Chatbot

Objective

Extend the chatbot so it stores a user profile separately from message history.

Desired State Structure

{
  "system_prompt": "...",
  "user_profile": {
    "name": "Maya",
    "favorite_language": "Python"
  },
  "history": [...]
}

Suggested Tasks

  1. Add a user_profile dictionary to state
  2. Let the user type commands such as:
  3. set name Maya
  4. set favorite_language Python
  5. Save these values in state
  6. Include profile information in the system prompt or as an extra message
  7. Ask the assistant to personalize its responses

Example Approach

def build_input_messages(state: dict) -> list:
    """
    Build model input using system prompt, user profile, and conversation history.
    """
    profile = state.get("user_profile", {})
    profile_lines = []

    if profile.get("name"):
        profile_lines.append(f"User name: {profile['name']}")
    if profile.get("favorite_language"):
        profile_lines.append(f"Favorite language: {profile['favorite_language']}")

    profile_text = "\n".join(profile_lines) if profile_lines else "No profile information available."

    return [
        {
            "role": "system",
            "content": state["system_prompt"]
        },
        {
            "role": "system",
            "content": f"Known user profile:\n{profile_text}"
        },
        *state["history"]
    ]

Example Interaction

You: set name Maya
Saved profile field: name = Maya

You: set favorite_language Python
Saved profile field: favorite_language = Python

You: What do you know about me?
Assistant: Your name is Maya, and your favorite language is Python.

8. Wrap-up

Key Ideas from This Session

  • State is the data an application remembers over time
  • Stateless functions are simple and predictable
  • Stateful applications are essential for interactive systems
  • Python state can be managed using variables, dictionaries, classes, and files
  • GenAI apps rely heavily on well-managed conversation and session state
  • The OpenAI Responses API can be used in Python to build state-aware applications

What Learners Should Now Be Able to Do

  • Represent application state in Python
  • Update state safely and clearly
  • Persist state to a JSON file
  • Build a simple stateful chatbot using gpt-5.4-mini
  • Think more carefully about how context is preserved in AI apps

Useful Resources

  • OpenAI Responses API migration guide: https://developers.openai.com/api/docs/guides/migrate-to-responses
  • OpenAI API docs: https://platform.openai.com/docs
  • OpenAI Python SDK: https://github.com/openai/openai-python
  • Python json module documentation: https://docs.python.org/3/library/json.html
  • Python pathlib documentation: https://docs.python.org/3/library/pathlib.html

Suggested Homework

  1. Modify the persistent chatbot to store timestamps for every message
  2. Add a command to show the last 5 conversation turns
  3. Add a command to export the conversation to a text file
  4. Add a summary field that stores a short conversation summary after every 5 turns
  5. Refactor the chatbot state into a Python class

Quick Recap Questions

  1. What is application state?
  2. How is a stateful program different from a stateless one?
  3. Why do GenAI chat applications need state?
  4. When would you use a dictionary versus a class for state?
  5. Why might you save state to disk?

Back to Chapter | Back to Master Plan | Previous Session | Next Session