Python Object Pooling — Core Concepts
Why Object Pooling Exists
Some objects are expensive to create. A database connection requires DNS resolution, TCP handshake, authentication, and protocol negotiation — easily 50–200 milliseconds. If every database query creates and destroys a connection, those milliseconds add up fast.
Object pooling solves this by maintaining a collection of pre-initialized, reusable objects. Instead of create → use → destroy, the lifecycle becomes borrow → use → return.
How It Works
A basic object pool has three operations:
- Get — Take an available object from the pool. If the pool is empty, either create a new one or wait.
- Use — Work with the object normally.
- Return — Put the object back in the pool for someone else to use.
Most pools also handle:
- Maximum size — Cap how many objects exist simultaneously to prevent resource exhaustion.
- Health checks — Verify returned objects are still valid before handing them out again.
- Idle cleanup — Destroy objects that have sat unused too long.
Built-In Pooling in Python
Python already pools several things without you asking:
- Small integers (-5 to 256) — Pre-allocated at interpreter startup and reused.
- Interned strings — Common strings share single objects in memory.
- Free lists — CPython maintains internal free lists for floats, tuples, and other frequently created types, recycling their memory blocks instead of returning them to the OS.
Connection Pooling: The Most Common Use Case
Database connection pools are by far the most practical application. Libraries like SQLAlchemy, psycopg2, and asyncpg all support connection pooling:
from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@host/db",
pool_size=10, # Keep 10 connections ready
max_overflow=5, # Allow 5 more under pressure
pool_recycle=3600, # Recreate connections after 1 hour
)
With this setup, the first 10 queries borrow from the pool instantly. Queries 11–15 create overflow connections. Query 16 waits until a connection is returned.
HTTP session pooling works similarly. The requests library’s Session object reuses TCP connections via urllib3’s connection pool:
import requests
session = requests.Session()
# All requests through this session reuse connections
session.get("https://api.example.com/users")
session.get("https://api.example.com/orders") # Same TCP connection
Building a Simple Pool
For custom objects, a queue-based pool works well:
import queue
class ObjectPool:
def __init__(self, factory, max_size=10):
self._factory = factory
self._pool = queue.Queue(maxsize=max_size)
self._size = 0
self._max_size = max_size
def get(self, timeout=None):
try:
return self._pool.get_nowait()
except queue.Empty:
if self._size < self._max_size:
self._size += 1
return self._factory()
return self._pool.get(timeout=timeout)
def put(self, obj):
self._pool.put_nowait(obj)
The queue.Queue is thread-safe, making this pool safe for multi-threaded applications without additional locking.
When Pooling Hurts
Pooling adds complexity and isn’t always worth it:
- Cheap objects — If creating the object takes microseconds (regular Python objects, small data structures), the pool overhead exceeds the creation cost.
- Stateful contamination — If objects retain state between uses (dirty buffers, stale cursors), bugs from forgotten cleanup are subtle and dangerous.
- Memory pressure — Pool objects stay in memory even when idle. A pool of 50 large buffers consumes memory whether they’re being used or not.
- Oversized pools — A pool of 100 database connections when you only ever use 5 wastes server resources and may hit database connection limits.
Common Misconception
Developers sometimes think pooling makes everything faster. In CPython, general object allocation is already quite fast thanks to the internal memory allocator (pymalloc). Pooling plain Python objects — dictionaries, lists, custom classes — rarely provides measurable benefit. The win comes specifically from objects with expensive initialization: I/O connections, GPU contexts, compiled regex patterns, and large pre-allocated buffers.
The one thing to remember: Object pooling trades memory for speed by keeping expensive-to-create objects alive and reusable — it’s essential for database connections and network sockets, but overkill for ordinary Python objects.
See Also
- Python Algorithmic Complexity Understand Algorithmic Complexity through a practical analogy so your Python decisions become faster and clearer.
- Python Async Performance Tuning Making your async Python faster is like organizing a busy restaurant kitchen — it's all about flow.
- Python Benchmark Methodology Why timing Python code once means nothing, and how fair testing works like a science experiment.
- Python C Extension Performance How Python borrows C's speed for the hard parts — like hiring a specialist for the toughest job on the worksite.
- Python Caching Strategies Understand Python caching strategies with a shortcut-road analogy so your app gets faster without taking wrong turns.