Sitemap

Q-Learning Explained: A Simple Guide with Python Examples

9 min readAug 10, 2026

--

You know what’s funny? Q-Learning is one of those algorithms that sounds terrifying when you first hear about it, but once it clicks, you’ll wonder why anyone made it seem so complicated. It’s basically just a fancy way of learning from trial and error — something you’ve been doing since you were a kid.

I remember the first time I tried to understand Q-Learning. I read some academic paper that threw equations at me like I was supposed to already know what they meant. Total disaster. But then I actually implemented it myself, watched it work, and suddenly everything made sense. That’s what we’re doing here — I’m going to show you Q-Learning in a way that actually makes sense, with real Python code you can run right now.

What Exactly Is Q-Learning?

Let’s start simple. Q-Learning is a reinforcement learning algorithm that helps an agent learn the best action to take in any situation. The “Q” stands for quality — as in, how good is this action in this particular state?

Think about playing a video game. You’re in some situation (that’s your state), and you have several buttons you could press (those are your actions). Q-Learning figures out which button to press by learning from experience. Press the wrong button and die? That’s negative feedback. Press the right button and win? Positive feedback.

Here’s the beautiful part: Q-Learning doesn’t need a manual. It doesn’t need someone to tell it the rules of the game or the perfect strategy. It just tries stuff, sees what happens, and gradually learns what works. That’s why it’s called a model-free algorithm.

The agent maintains a Q-table — basically a cheat sheet that says “in this state, this action is worth this much.” Over time, through exploration and updates, this table gets better and better until the agent knows the optimal strategy.

The Q-Value: Your Action Report Card

At the heart of Q-Learning is the Q-value. This is a number that estimates how good it is to take a specific action in a specific state, then follow your best strategy afterward.

Let’s say you’re playing a maze game. You’re at position (2, 3) and you can move up, down, left, or right. Each of these actions has a Q-value:

  • Q(state=(2,3), action=up) = 5.2 — Pretty good move
  • Q(state=(2,3), action=right) = 8.7 — Even better
  • Q(state=(2,3), action=down) = -3.1 — Bad idea, probably a pit
  • Q(state=(2,3), action=left) = 1.4 — Meh, mediocre

The agent picks the action with the highest Q-value. Simple as that. The trick is learning accurate Q-values in the first place.

The Q-Learning Update Rule

Here’s where the magic happens. After taking an action and seeing the result, the agent updates its Q-value using this formula:

Q(s,a) ← Q(s,a) + α[r + γ × max Q(s’,a’) — Q(s,a)]

I know, I know — equations are annoying. But stick with me because this one is actually intuitive once you break it down.

Let’s decode each piece:

  • Q(s,a) — Current Q-value for state s and action a
  • α (alpha) — Learning rate (how much you update, usually 0.1 to 0.5)
  • r — Reward you just received
  • γ (gamma) — Discount factor (how much you care about future rewards, usually 0.9 to 0.99)
  • max Q(s’,a’) — Best possible Q-value in the new state s’
  • Q(s,a) again — Your old estimate

The formula basically says: “Take your old estimate, and nudge it toward what you just learned.” The part in brackets is the temporal difference error — the gap between what you expected and what actually happened.

Building a Simple Q-Learning Agent in Python

Enough theory. Let’s build something. We’ll create a simple grid world where an agent learns to reach a goal while avoiding obstacles.

python

import numpy as np
import random
class QLearningAgent:
def __init__(self, states, actions, learning_rate=0.1, discount_factor=0.95, epsilon=1.0):
self.q_table = np.zeros((states, actions))
self.lr = learning_rate
self.gamma = discount_factor
self.epsilon = epsilon
self.epsilon_decay = 0.995
self.epsilon_min = 0.01

def choose_action(self, state, available_actions):
# Exploration vs exploitation
if random.uniform(0, 1) < self.epsilon:
return random.choice(available_actions) # Explore
else:
q_values = self.q_table[state, available_actions]
return available_actions[np.argmax(q_values)] # Exploit

def update(self, state, action, reward, next_state):
# Q-Learning update formula
current_q = self.q_table[state, action]
max_next_q = np.max(self.q_table[next_state])
new_q = current_q + self.lr * (reward + self.gamma * max_next_q - current_q)
self.q_table[state, action] = new_q

def decay_epsilon(self):
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)

This is a basic Q-Learning agent. Nothing fancy, but it has everything you need. The q_table stores all the Q-values, the choose_action method implements exploration vs exploitation, and the update method is the Q-Learning formula in action.

Creating a Grid World Environment

Now let’s build a simple environment for our agent to learn in:

python

class GridWorld:
def __init__(self, size=5):
self.size = size
self.goal = (size-1, size-1)
self.obstacles = [(1, 1), (2, 2), (3, 1)]
self.reset()

def reset(self):
self.agent_pos = (0, 0)
return self.state_to_index(self.agent_pos)

def state_to_index(self, pos):
return pos[0] * self.size + pos[1]

def get_available_actions(self):
# 0: up, 1: down, 2: left, 3: right
actions = []
x, y = self.agent_pos

if x > 0: actions.append(0) # Can move up
if x < self.size - 1: actions.append(1) # Can move down
if y > 0: actions.append(2) # Can move left
if y < self.size - 1: actions.append(3) # Can move right

return actions

def step(self, action):
x, y = self.agent_pos

# Execute action
if action == 0: x -= 1 # Up
elif action == 1: x += 1 # Down
elif action == 2: y -= 1 # Left
elif action == 3: y += 1 # Right

new_pos = (x, y)

# Calculate reward
if new_pos == self.goal:
reward = 100
done = True
elif new_pos in self.obstacles:
reward = -100
done = True
else:
reward = -1 # Small penalty for each step
done = False

self.agent_pos = new_pos
return self.state_to_index(new_pos), reward, done

This grid world is pretty straightforward. The agent starts at the top-left corner and needs to reach the bottom-right corner. There are some obstacles to avoid, and each step costs a small penalty to encourage the agent to find the shortest path.

Training the Agent

Now comes the fun part — watching the agent learn:

python

def train_agent(episodes=1000):
env = GridWorld(size=5)
agent = QLearningAgent(states=25, actions=4)

rewards_per_episode = []

for episode in range(episodes):
state = env.reset()
total_reward = 0
steps = 0

while steps < 100: # Max steps per episode
available_actions = env.get_available_actions()
action = agent.choose_action(state, available_actions)

next_state, reward, done = env.step(action)
agent.update(state, action, reward, next_state)

total_reward += reward
state = next_state
steps += 1

if done:
break

agent.decay_epsilon()
rewards_per_episode.append(total_reward)

if episode % 100 == 0:
avg_reward = np.mean(rewards_per_episode[-100:])
print(f"Episode {episode}, Avg Reward: {avg_reward:.2f}, Epsilon: {agent.epsilon:.3f}")

return agent, rewards_per_episode
# Train the agent
trained_agent, rewards = train_agent(episodes=1000)

Run this code and you’ll see the agent get better over time. Early episodes might show terrible performance as the agent randomly explores. But gradually, the average reward increases as the agent learns the optimal path.

Exploration vs Exploitation: The Eternal Dilemma

One of the trickiest parts of Q-Learning is balancing exploration and exploitation. Should your agent try new things or stick with what it knows works?

Get Sam Austin AI’s stories in your inbox

Join Medium for free to get updates from this writer.

In our code, we use epsilon-greedy exploration. The agent has an epsilon value (starting at 1.0) that represents the probability of choosing a random action. As training progresses, epsilon decays, so the agent explores less and exploits more.

Think of it like learning to cook. At first, you try lots of random ingredients (high exploration). Once you figure out what works, you stick with good recipes (exploitation). But you still occasionally try new things, or you’d never discover anything better.

This balance is crucial. Too much exploration? Your agent never settles on a good strategy. Too little? It gets stuck with the first mediocre solution it finds. IMO, starting with high epsilon and gradually decreasing it is one of those simple tricks that just works.

Understanding the Learning Rate and Discount Factor

Two hyperparameters seriously impact how your agent learns: learning rate (α) and discount factor (γ).

Learning rate controls how much new information overrides old information. Set it too high (like 0.9), and your agent has amnesia — it forgets everything it learned before. Set it too low (like 0.01), and learning is painfully slow. Usually, 0.1 to 0.3 works well.

Discount factor determines how much the agent cares about future rewards. A value of 0.95 means future rewards are worth 95% of immediate rewards. Set it to 0, and your agent only cares about instant gratification. Set it to 1, and future rewards matter just as much as immediate ones.

For most problems, gamma between 0.9 and 0.99 is the sweet spot. You want your agent to be somewhat forward-thinking but not infinitely so.

Visualizing What the Agent Learned

After training, you can visualize the learned policy:

python

def print_policy(agent, size=5):
action_symbols = {0: '↑', 1: '↓', 2: '←', 3: '→'}

for i in range(size):
row = []
for j in range(size):
state = i * size + j
best_action = np.argmax(agent.q_table[state])
row.append(action_symbols[best_action])
print(' '.join(row))
print("\nLearned Policy:")
print_policy(trained_agent)

This shows you exactly what action the agent prefers in each position. You’ll see arrows pointing toward the goal, flowing around obstacles like water around rocks. It’s honestly pretty cool to watch :)

Common Pitfalls and How to Fix Them

After implementing Q-Learning dozens of times, here are the mistakes I see constantly (and made myself plenty of times).

Problem: Agent never learns anything Usually means your learning rate is too low or your rewards are poorly scaled. If rewards are tiny compared to the Q-values, updates barely change anything. Scale your rewards or increase the learning rate.

Problem: Q-values explode to infinity Your discount factor is probably too high or you have a reward loop. Check that your environment eventually terminates and that gamma is less than 1.

Problem: Agent learns then forgets Learning rate too high. The agent overwrites good knowledge with new experiences. Lower alpha to something like 0.1.

Problem: Agent gets stuck in local optimum Not enough exploration. Increase initial epsilon or slow down the decay rate. The agent needs to explore more before settling on a strategy.

Problem: Training takes forever Welcome to RL :/ But seriously, try increasing the learning rate slightly, simplify your environment for testing, or add reward shaping to guide the agent toward good solutions faster.

When Q-Learning Works (and When It Doesn’t)

Q-Learning is awesome for certain problems but terrible for others. Let’s be real about its limitations.

Q-Learning excels when:

  • State and action spaces are discrete and relatively small
  • You can afford lots of exploration
  • Rewards are reasonably frequent
  • The environment is relatively simple

Q-Learning struggles when:

  • State or action spaces are continuous or huge (use Deep Q-Learning instead)
  • Rewards are extremely sparse
  • The environment is highly stochastic
  • You need immediate good performance without training

For the classic grid world, frozen lake, or taxi problems? Q-Learning is perfect. For playing StarCraft or controlling a robotic arm? You’ll need more sophisticated approaches like Deep Q-Networks or policy gradient methods.

Taking It Further

Once you’ve got basic Q-Learning working, here are some fun extensions to try:

Double Q-Learning — Reduces overestimation bias by maintaining two Q-tables and using them to check each other. Surprisingly simple modification with real benefits.

Prioritized Experience Replay — When you store experiences, replay important ones more often. Speeds up learning significantly.

Multi-step Q-Learning — Instead of one-step updates, use rewards from multiple steps ahead. Propagates information faster through the Q-table.

Function Approximation — Replace the Q-table with a neural network. Now you can handle massive state spaces. This is the bridge to Deep Q-Learning.

You can also try different environments. OpenAI Gym (now Gymnasium) has tons of pre-built environments where you can test your Q-Learning agent. FYI, watching your agent fail hilariously in new environments is both educational and entertaining.

Your Turn to Build Something

Here’s the thing about Q-Learning — reading about it only gets you so far. You need to implement it, break it, fix it, and watch it learn. That’s where the real understanding comes from.

Start with the code I showed you. Run it. Modify the grid size. Add more obstacles. Change the rewards. See what breaks and what works better. This hands-on experimentation teaches you more than any textbook ever could.

Then build something new. Maybe a simple game, or a pathfinding agent, or something that solves a problem you actually care about. The principles are the same regardless of the domain.

Q-Learning is one of those algorithms that feels like magic when you first see it work. An agent that literally learns from nothing, figuring out strategies through trial and error. It’s the foundation of modern reinforcement learning, and understanding it deeply opens doors to everything else in the field.

So stop reading and start coding. Your first Q-Learning agent is waiting to be born.

--

--

A lifelong learner! I enjoy learning &sharing things as possible. If you like my stories and would love to support more https://buymeacoffee.com/samaustin