The Factory Method pattern hands the decision of which concrete class to instantiate to a separate function or method, so calling code asks for a thing by name or by config and gets back an object that satisfies a common interface. In Java this usually means an abstract creator class with subclasses. Python rarely needs that machinery: a plain function, a dict lookup, or a classmethod does the same job because classes are first-class values you can store and pass around.
A registry-based factory
The most common Python form is a dict that maps a string key to a class (or a callable that builds the object). A data pipeline that reads from several sources is the textbook case: the config says "postgres" or "s3", and the factory returns the right connector without the caller importing or naming the concrete class. New backends register themselves, and the caller never changes.
from typing import Protocol
class Connector(Protocol):
def read(self, query: str) -> list[dict]: ...
class PostgresConnector:
def __init__(self, dsn: str):
self.dsn = dsn
def read(self, query: str) -> list[dict]:
return [{"src": "postgres", "q": query}]
class S3Connector:
def __init__(self, bucket: str):
self.bucket = bucket
def read(self, query: str) -> list[dict]:
return [{"src": "s3", "key": query}]
_REGISTRY: dict[str, type[Connector]] = {
"postgres": PostgresConnector,
"s3": S3Connector,
}
def make_connector(kind: str, **kwargs) -> Connector:
try:
cls = _REGISTRY[kind]
except KeyError:
raise ValueError(f"unknown connector: {kind!r}") from None
return cls(**kwargs)
conn = make_connector("postgres", dsn="postgresql://localhost/db")
print(conn.read("SELECT 1"))
Classmethod factories for alternative constructors
When the variation is "build the same class different ways" rather than "pick a different class," the Pythonic Factory Method is a classmethod that returns an instance. The standard library uses this constantly - dict.fromkeys, datetime.fromtimestamp, int.from_bytes. For a parser, you might expose from_path and from_string so callers don't juggle file handles versus raw text.
from pathlib import Path
class CsvParser:
def __init__(self, rows: list[list[str]]):
self.rows = rows
@classmethod
def from_string(cls, text: str) -> "CsvParser":
return cls([line.split(",") for line in text.splitlines() if line])
@classmethod
def from_path(cls, path: str | Path) -> "CsvParser":
return cls.from_string(Path(path).read_text(encoding="utf-8"))
p = CsvParser.from_string("a,b\n1,2")
print(p.rows) # [['a', 'b'], ['1', '2']]
When a function is enough
The pattern's intent is decoupling the caller from concrete classes, not building a class hierarchy. If you find yourself writing an abstract Creator base with one create() override per subclass, ask whether a module-level function or a dict gets you the same indirection with less ceremony. Reserve the heavier subclass form for cases where the creator itself holds state or has behavior beyond instantiation - for example, a connection pool that hands out connectors and tracks them.
Frequently asked questions
Factory Method vs. Abstract Factory? Factory Method produces one product through a single creator; Abstract Factory produces a family of related products through a creator object with several methods. In Python both often collapse into a function or a small registry.
Do I need an abstract base class for this? No. Duck typing or a typing.Protocol lets callers depend on the interface without an inheritance hierarchy. Use ABC only if you want runtime enforcement that subclasses implement the methods.
How do plugins register with the factory? Either populate the registry dict explicitly at import, use a decorator that inserts the class into the dict, or discover them through importlib.metadata.entry_points for third-party packages.
Where does this show up in data engineering? Choosing a source connector, a file parser, or a serialization format from config; building the right warehouse client per environment; selecting a model loader by framework name.