SimPy Container Explained: Modelling Levels and Quantities

Not everything is about counting slots. Sometimes you need to model levels—fuel in a tank, items in a buffer, water in a reservoir. That's what Container is for.

Resource vs Container

Resource: Discrete slots. "3 of 5 tills occupied."

Container: Continuous levels. "Tank at 750 of 1000 litres."

import simpy

env = simpy.Environment()

# Resource: counting slots
checkout = simpy.Resource(env, capacity=5)

# Container: measuring levels
fuel_tank = simpy.Container(env, capacity=1000, init=500)

Basic Usage

env = simpy.Environment()
tank = simpy.Container(env, capacity=100, init=50)

print(tank.level)     # 50
print(tank.capacity)  # 100

Put and Get

Add to or remove from a container:

def producer(env, container):
    while True:
        yield env.timeout(5)
        yield container.put(10)  # Add 10 units
        print(f"Added 10, level now {container.level}")

def consumer(env, container):
    while True:
        yield env.timeout(3)
        yield container.get(5)  # Remove 5 units
        print(f"Removed 5, level now {container.level}")

Blocking Behaviour

put() blocks when container is full:

tank = simpy.Container(env, capacity=100, init=95)
yield tank.put(10)  # Blocks until there's room for 10

get() blocks when container is empty:

tank = simpy.Container(env, capacity=100, init=3)
yield tank.get(10)  # Blocks until there's at least 10

This is the magic—processes automatically wait for the right conditions.

Fuel Station Example

import simpy
import random

def car(env, name, station, tank_size):
    """Car arrives, fills up, leaves."""
    fuel_needed = random.uniform(20, 50)

    print(f"{name} arrives, needs {fuel_needed:.0f}L at {env.now:.1f}")

    yield station.get(fuel_needed)  # Pump fuel

    print(f"{name} filled up at {env.now:.1f}, station level: {station.level:.0f}")

def tanker(env, station):
    """Tanker periodically refills station."""
    while True:
        yield env.timeout(50)  # Arrive every 50 time units
        refill = station.capacity - station.level
        yield station.put(refill)
        print(f"Tanker delivered {refill:.0f}L at {env.now:.1f}")

def generate_cars(env, station):
    """Generate arriving cars."""
    i = 0
    while True:
        yield env.timeout(random.expovariate(1/5))
        env.process(car(env, f"Car{i}", station, 60))
        i += 1

env = simpy.Environment()
station = simpy.Container(env, capacity=500, init=300)
env.process(generate_cars(env, station))
env.process(tanker(env, station))
env.run(until=200)

Manufacturing Buffer

def machine(env, name, input_buffer, output_buffer, process_time):
    """Machine takes from input, processes, puts to output."""
    while True:
        yield input_buffer.get(1)  # Take 1 item
        yield env.timeout(process_time)  # Process
        yield output_buffer.put(1)  # Output 1 item
        print(f"{name} processed at {env.now}")

env = simpy.Environment()
raw_materials = simpy.Container(env, capacity=100, init=50)
finished_goods = simpy.Container(env, capacity=100, init=0)

env.process(machine(env, "Lathe", raw_materials, finished_goods, 5))
env.run(until=100)

Checking Without Blocking

Sometimes you want to check if operation is possible:

def try_get(container, amount):
    """Non-blocking get attempt."""
    if container.level >= amount:
        yield container.get(amount)
        return True
    return False

Or use events with timeout:

def consumer(env, container, patience):
    get_event = container.get(10)
    timeout_event = env.timeout(patience)

    result = yield get_event | timeout_event

    if get_event in result:
        print("Got the items")
    else:
        get_event.cancel()  # Important!
        print("Gave up waiting")

Container Properties

container = simpy.Container(env, capacity=100, init=30)

container.level      # Current level (30)
container.capacity   # Maximum capacity (100)
len(container.put_queue)  # Processes waiting to put
len(container.get_queue)  # Processes waiting to get

Monitoring Levels

level_log = []

def monitor(env, container, interval):
    while True:
        level_log.append({
            'time': env.now,
            'level': container.level
        })
        yield env.timeout(interval)

env.process(monitor(env, tank, 1))

Visualise after:

import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame(level_log)
plt.plot(df['time'], df['level'])
plt.xlabel('Time')
plt.ylabel('Level')
plt.show()

Partial Operations

Unlike Resources, Containers allow partial operations by design. You're moving quantities, not discrete items.

# Valid: add 5.5 units
yield container.put(5.5)

# Valid: remove 3.7 units
yield container.get(3.7)

Use integers if your domain requires them.

When to Use Container

Use Container for: - Fuel tanks, water reservoirs - Inventory levels - Production buffers - Bank account balances - Any continuous quantity

Use Resource for: - Staff, machines, servers - Discrete slots - Things you "borrow" and "return"

Common Gotchas

Not checking level before get

# This might block forever if level never reaches 10
yield container.get(10)

Consider timeouts or checking level first.

Forgetting initial level

# Default init is 0
tank = simpy.Container(env, capacity=100)
yield tank.get(50)  # Blocks! Tank is empty

Put more than capacity

tank = simpy.Container(env, capacity=100, init=50)
yield tank.put(60)  # Blocks until level <= 40

Summary

Container: - Models continuous levels, not discrete slots - Use put(amount) to add, get(amount) to remove - Blocks when full (put) or empty (get) - Tracks .level and .capacity

For tanks, buffers, and inventories—Container is your tool.

Next Steps


Discover the Power of Simulation

Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.

Explore the Bootcamp