Solving the "Chicken-Egg" problem with Python
Tags: python, python37, imports, circulardependencies
Published: 2019-08-10 21:10:52

---
title: Solving the "Chicken-Egg" problem with Python
published: true
description: 
tags: Python, Python3.7, imports, circular dependencies
---

Hello!

Nothing could be more tasty to Python than chicken and eggs. So today our Python decided to resolve an old phylosophical question: what was the first, chicken or egg. In order to do it, it has decided to create two classes and then to perform some modelling to make an ultimate answer to this question.

Ok, let's start. Our Python is a grand-pa of all pythons, and it's serial number is 2.7. To begin with, it created 3 files:

```
# filename: models/egg.py

import models.chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return models.chicken.Chicken("Chicken from [{self.name}]".format(self=self))
```

```
# filename: models/chicken.py

import models.egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return models.egg.Egg("Egg of [{self.name}]".format(self=self))
```

```
# filename: main.py

import models.chicken


if __name__ == '__main__':
    # Decision maker!
    c = models.chicken.Chicken("Ryaba")
    print(c.create_egg().wait().create_egg().name)
```

Grand-pa Python is happy with the results:

```
$ python2.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]
```

For those of you who are sceptical about the circular dependencies in Python - yes, they are supported, but only to some extent. Generally, when the interpreter sees `import models.egg` it checks if this module is imported and if it is, uses the address of this module from the cache. If it is not, it immediately creates a record in the cache and then starts doing actual import. That's why if you only have `import <module_name>` statements in your code you are safe.

As soon as we decide to use `from <module_name> import <object>` statement, our circular dependencies will not be able to be resolved. Let's try it!

```
# filename: models/egg.py

from models.chicken import Chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return Chicken("Chicken from [{self.name}]".format(self=self))
```

```
# filename: models/chicken.py

from models.egg import Egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return Egg("Egg of [{self.name}]".format(self=self))
```

```
$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models.egg import Egg
  File "/data/models/egg.py", line 1, in <module>
    from models.chicken import Chicken
ImportError: cannot import name Chicken

$ python3.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models.egg import Egg
  File "/data/models/egg.py", line 1, in <module>
    from models.chicken import Chicken
ImportError: cannot import name 'Chicken' from 'models.chicken' (/data/models/chicken.py)
```

This is more or less predictable and it is a real chicken-egg problem. In order to import `Chicken` class to the `egg` module you need to parse `chicken` module (so, just an address of a module is not enough), and in order to completely parse `chicken` module, you need to complete parsing of an `egg` module. So, no way.

OK. And now I need your full attention: we are going to replace `import <package>.<module>` by `from <package> import <module>`.

```
# filename: models/egg.py

from models import chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return chicken.Chicken("Chicken from [{self.name}]".format(self=self))
```

```
# filename: models/chicken.py

from models import egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return egg.Egg("Egg of [{self.name}]".format(self=self))
```

And the result is a bit unexpectable:

```
$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models import egg
  File "/data/models/egg.py", line 1, in <module>
    from models import chicken
ImportError: cannot import name chicken

$ python3.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]
```

So, Python3 changed the way how packages are imported, generally they are appended to the sys.modules earlier than Python2. Good to know.

And the last surprise for today. Now we are using `import <package>.<module> as <alias>` syntax.

```
# filename: models/egg.py

import models.chicken as chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return chicken.Chicken("Chicken from [{self.name}]".format(self=self))
```

```
# filename: models/chicken.py

import models.egg as egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return egg.Egg("Egg of [{self.name}]".format(self=self))
```

And can you predict the output?

```
$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    import models.egg as egg
  File "/data/models/egg.py", line 1, in <module>
    import models.chicken as chicken
AttributeError: 'module' object has no attribute 'chicken'

$ python3.6 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    import models.egg as egg
  File "/data/models/egg.py", line 1, in <module>
    import models.chicken as chicken
AttributeError: module 'models' has no attribute 'chicken'

$ python3.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]
```

Python3.7 won :) Ok. What is really happened here is that there was a [bug](https://bugs.python.org/issue30024) in the `import A.B.C as D` syntax which was [fixed](https://github.com/python/cpython/pull/3217/files) in the version 3.7.

Thanks for reading!