blog banner alt

Factory Pattern in Python 3 - The Simple Version

Last updated Nov 24, 2017


If you came from another programming language background, you probably already know what “factory method design pattern” means in software development. To put it in a simple manner, Factory Method Pattern is a design pattern which you let a function / method to handle the creation of an object.

I’m sure you have probably come across other tutorials on how to create a factory in Python. But what if I tell you, there is even an easier and more scalable way of getting the job done?

Like without creating a specific factory class? And oh, the bigger advantage is you can make as many extended class to the base class as you want without having to modify the factory method itself.

Because of the nature of Python, it is very easy to make a factory simply by taking full advantage of the features in Python.

In this tutorial, we will be using Python 3.6. You can find the source code in this tutorial here.

Interested?

Well, let’s get started!

 

Goal

As an example, our goal is to create an animal factory to help grabbing us the animals we want. Although I’d rather not call this a ‘factory’ when it has to do with animals… ( but this is almost like a pet shop, right? <3)

Joke aside, we want to be able to get animals like this:

my_cat = animals.grab('cat', name='kitty')
my_cat.make_sound()

As you can see, my_cat is an ‘cat’ object that we need to implement, and by calling animal.grab(), it fetches this object for us dynamically dependent on the first string argument we pass into the function.

If this sounds confusing to you, don’t worry. I promise you by the end of this tutorial, you can fully understand how this works.

 

Project Structure

To structure our project, we will have an overall directory called animal_project, then in this directory, we will make two additional directories: animals and tests.

It’s always good to have unit tests for any code you write. I won’t be covering unit test in this article, perhaps another time.

Here is the overall file structure:

animal_project/
animal_project/animals
animal_project/animals/__init__.py
animal_project/animals/base_animal.py
animal_project/animals/cat.py
animal_project/animal/dog.py
animal_project/tests /__init__.py
animal_project/tests/test_animals.py

At this point, I would really think of animals/ directory as our pet shop. Let’s go ahead and take a look inside of the pet shop:

  • base_animal.py:It should consist of a single class: AnimalBaseClass, which will be our abstract base class all the other animals will extend from.
  • cat.py and dog.py: Module for classes which will extend from this AnimalBaseClass.
  • __init__.py: this is where we will place our factory method – grab().

Now, let’s start filling out these files one by one.

 

AnimalBaseClass

As mentioned previously, this class will be our base class. In order for this to work, for all animals, doesn’t matter if it’s a mammal or reptile, all have to extend from this class.

This is how it would look like:

animal_project/animals/base_animal.py

from abc import ABCMeta, abstractmethod


class AnimalBaseClass(metaclass=ABCMeta):
    def __init__(self, name=None):
        if name:
            self.name = name

    @abstractmethod
    def make_sound(self):
        pass

    @abstractmethod
    def move(self):
        pass

I like to make our base class as an abstract base class, just so to prevent instantiation, although this is recommended, but it is optional.

We have made two abstract methods: make_sound(),  and move(). This means any animal extend from the base class will need to implement these two methods.

 

Animal Classes

Now we have a base class, let’s get some animals in the pet shop!

Currently, we just have cat and dog.

animal_project/animals/cat.py

from . import AnimalBaseClass


class Cat(AnimalBaseClass):
    def __init__(self, name=None):
        AnimalBaseClass.__init__(self, name)

    def make_sound(self):
        print('meowwww! :3')

    def move(self):
        print('the cat walks')

    def purr(self):
        print('purrrrr!<3')

We have added another method, purr(), for the cats, because cats do purr..

animal_project/animals/dog.py

from . import AnimalBaseClass


class Dog(AnimalBaseClass):
    def __init__(self, name=None):
        AnimalBaseClass.__init__(self, name)

    def make_sound(self):
        print('woof, woof! :3')

    def move(self):
        print('the dog walks')

Both Cat and Dog class are extending from AnimalBaseClass, and using the parent __init__() method.

Okay, we have got the animals, let’s get to the fun part and make the factory method ( or should I say pet shop?)

 

Factory Method

__init__.py inside of animals/ directory is where we are going to place our factory method.

Why? Because in Python, whichever directory with an __init__.py is being treated as a package that you can import, and the need for creating a separate class for the factory becomes unnecessary. The directory animal/ can be technically treated as an entity of its own (like a pet shop!)

You might be used to having __init__.py completely empty, but this file is actually quite useful. Now is your chance to take advantage of it!

Inside of  __init__.py, we will have a single method which is our factory method, called grab().

Let’s take a look:

animal_project/animals/__init__.py

from importlib import import_module

from .base_animal import AnimalBaseClass


def grab(animal_name, *args, **kwargs):

    try:
        if '.' in animal_name:
            module_name, class_name = animal_name.rsplit('.', 1)
        else:
            module_name = animal_name
            class_name = animal_name.capitalize()

        animal_module = import_module('.' + module_name, package='animals')

        animal_class = getattr(animal_module, class_name)

        instance = animal_class(*args, **kwargs)

    except (AttributeError, ModuleNotFoundError):
        raise ImportError('{} is not part of our animal collection!'.format(animal_name))
    else:
        if not issubclass(animal_class, AnimalBaseClass):
            raise ImportError("We currently don't have {}, but you are welcome to send in the request for it!".format(animal_class))

    return instance

importlib is a very useful library, which allows you to import modules dynamically given a string. We are using the import_module() here to help us import the animal module we need, and assigning the imported module as a variable animal_module. This is equivalent to “import animal.cat as animal_module”.

Then we use Python’s builtin getattr() to get the class we need. In this case, the class name is the same as the module name, so we will be using the same string passed with the first letter capitalized. ( take note it is important that class name and module name to be following the same convention for all extended classes.)

After getting the animal class, with a try except and else clause, we check if the class and the module exist. Then we also need to check if the animal is extended class of our AnimalBaseClass, we can catch all the errors to do with the importation at once, then throw an exception with some custom messages.

Right here, I used the builtin ImportError, but I would recommend using a custom exception here. As this article is about factory (pet shop) design pattern, I won’t go into details about exception handling.

And then, our factory method is done!

Why don’t we write some tests?

 

Tests:

In this project, I’m using pytest for our unit testing. It is a very extensive and easy to use framework for testing in Python.

Here is our test:

animal_project/tests/test_animals.py

import animals


class TestAnimals:
    def test_cat_init(self, capsys):
        cat_instance = animals.grab('cat', name='kitty')

        cat_instance.make_sound()
        out, err = capsys.readouterr()
        assert out == 'meowwww! :3\n'

        cat_instance.move()
        out, err = capsys.readouterr()
        assert out == 'the cat walks\n'

        cat_instance.purr()
        out, err = capsys.readouterr()
        assert out == 'purrrrr!<3\n'

    def test_dog_init(self, capsys):
        cat_instance = animals.grab('dog')

        cat_instance.make_sound()
        out, err = capsys.readouterr()
        assert out == 'woof, woof! :3\n'

        cat_instance.move()
        out, err = capsys.readouterr()
        assert out == 'the dog walks\n'

run it, all test passes!<3

 

Closing note:

In the source code on Github, I have added a bit more tweak to this. Which allows our animal package to have subdirectories for categorizing each animal. As you can see in the fish directory, I added some code in the __init__.py file for dynamically import. I will be covering this in my next post, stay tuned!