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:

  1. Get — Take an available object from the pool. If the pool is empty, either create a new one or wait.
  2. Use — Work with the object normally.
  3. 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.

pythonperformancememorydesign-patterns

See Also