Disclaimer: This blog is not a guide on how to implement the Factory Pattern. It simply uses it as a basic example for a use case of generics.

Table Of Contents

The Problem

Factory Pattern Example

Let’s start with the problem we are trying to solve - using the Factory Pattern to help us create objects.

We typically might start with something like this:

import abc

class BaseObject(abc.ABC):
		...

class ObjectFactory(abc.ABC):
    def create(self, *args, **kwargs) -> BaseObject:
		    ...

class MyCustomObject1(BaseObject):
		...

class MyCustomObject2(BaseObject):
		...

factory = ObjectFactory()

instance: MyCustomObject1 = factory.create()  # uh-oh

Immediately, we encounter our first problem: How can I decide which type of BaseObject I want to create?

Someone who has just learned the foundations of Object Oriented Programming would like think about creating a child factory for each desired type explicitly. For example:

# Make the factory abstract
class BaseObjectFactory(abc.ABC):
		@abc.abstractmethod
    def create(self, *args, **kwargs) -> BaseObject:
		    raise NotImplementedError()

# Build a factory for MyCustomObject1
class MyCustomObject1Factory(BaseObjectFactory):
		@abc.abstractmethod
    def create(self, *args, **kwargs) -> BaseObject:
		    return MyCustomObject1(*args, **kwargs)
		    
# *Build a factory for MyCustomObject2*
...

# Instantiate our Factories
my_custom_object_1_factory = MyCustomObject1Factory()

instance: MyCustomObject1 = my_custom_object_1_factory.create()

But clearly, this is defeating the key purpose of the factory pattern… to help us build objects! This can easily over complicate things. Besides, a single factory should typically be abstract enough to handle multiple types.

Dynamic Factory using Generics

This is where generics come in. Rather than explicitly defining a new factory for each object type, we can make the factory produce objects of a dynamic type T.

from typing import TypeVar, Generic

T = TypeVar("T")

# Make the factory utilise generics
class ObjectFactory(Generic[T]):
    def create(self, *args, **kwargs) -> T:
		    raise T(*args, **kwargs)

# Instantiate our Factories
my_custom_object_1_factory = ObjectFactory[MyCustomObject1]()

instance: MyCustomObject1 = my_custom_object_1_factory.create()

And optionally, if you want maintain the factory create type hint to be bound to BaseObject, simply update the TypeVar definition:

T = TypeVar("T", bound=BaseObject)

Okay, so this has simplified things a bit. We no longer need to define a new factory for each object type!

But wait! If you run this code, you will encounter a TypeError: 'TypeVar' object is not callable , and using the debugger will clearly show you why:

Inspecting T reveals that T does not point to MyCustomObject1, but instead points to the TypeVar that we declared earlier.