Type hinting may cause cyclic imports

Recently I was working on rss3 SDK. In order to facilitate development, I just make a python version of the reference JavaScript SDK, which means the usage should be pretty similar between both.

Make a Python version

What’s more, to make a clear code,I use type hinting in the new project. However, there is a code snippet in JavaScript version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# index.ts
class RSS3 {
options: IOptions;
persona: Persona;
file: File;
profile: Profile;
items: Items;
item: Item;
links: Links;

constructor(options: IOptions) {
this.options = options;

this.file = new File(this);
this.persona = new Persona(this);
this.profile = new Profile(this);
this.items = new Items(this);
this.item = new Item(this);
this.links = new Links(this);
}
}
# file.ts
import Main from './index';
class File {
private main: Main;
private list: {
[key: string]: RSS3IContent;
} = {};
private dirtyList: {
[key: string]: number;
} = {};

constructor(main: Main) {
this.main = main;
}
...
}

Yes, the two files refer to each other. Actually, it happens on many other files. When I turned this into Python version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# index.py
class RSS3:
...
persona: Persona
file: File_
profile: Profile
items: Items
items: Item
links: Links

# file.py
from .index import RSS3

class File:
rss3: RSS3

def __init__(self, rss3: RSS3):
self.rss3 = rss3

circular import

It didn’t seem any errors, but when I started testing the problems appeared.

1
E   ImportError: cannot import name 'RSS3' from partially initialized module 'rss3.src.index' (most likely due to a circular import) 

how to solve this problem ? Don’t worry, PEP 484 has given a solution.

Solutions

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

So we can modify our code like the following:

1
2
3
4
5
class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

That’s okay already.

Sometimes there’s code that must be seen by a type checker (or other static analysis tools) but should not be executed. For such situations the typing module defines a constant, TYPE_CHECKING, that is considered True during type checking (or other static analysis) but False at runtime.

Modify out code again:

1
2
3
4
5
6
7
8
if TYPE_CHECKING:
from .index import RSS3

class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

What’s more

if we are using Python 3.7+, we can at least skip having to provide an explicit string annotation by taking advantage of PEP 563:

1
2
3
4
5
6
7
8
9
10
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .index import RSS3

class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

The from __future__ import annotations import will make all type hints be strings and skip evaluating them.

References

PEP 484 – Type Hints

Python type hinting without cyclic imports