A module is a .py file. A package is a directory of them. That is the whole vocabulary —
everything else in this post is about what import really does and why circular imports
happen.
A module is a file
Given money.py next to your script:
from pathlib import Path
Path("money.py").write_text(
'CENTS = 2\n'
'def format_money(value):\n'
' return f"${value:,.{CENTS}f}"\n',
encoding="utf-8",
)
import money
print(money.CENTS) # Output: 2
print(money.format_money(1250)) # Output: $1,250.00
The file name without .py is the module name. Importing it runs the file once and gives
you an object whose attributes are everything defined in it.
"Once" is important: a second import money anywhere in the program does not re-run it.
Python caches modules in sys.modules, so top-level code in a module executes exactly one
time no matter how many files import it.
The four forms of import
import json # json.dumps(...)
import json as j # j.dumps(...)
from decimal import Decimal # Decimal(...)
from decimal import Decimal as D # D(...)
print(json.dumps({"a": 1})) # Output: {"a": 1}
print(D("0.1") + Decimal("0.2")) # Output: 0.3
import x keeps the namespace, so the reader can see where dumps came from.
from x import y is shorter and worth it for something you use constantly.
What you should not write is from module import *. It dumps every public name into your
file, so you cannot tell what came from where, and a name added to that module later can silently
shadow one of yours.
Where Python looks
import sys
import json
print(isinstance(sys.path, list)) # Output: True
print("json" in sys.modules) # Output: True
print("sqlite3" in sys.modules) # Output: False
The last two lines show the cache: json is in sys.modules because the line
above imported it, and sqlite3 is not because nothing has. Importing json
again anywhere in this program would find it there and skip the file entirely.
sys.path is the list of directories searched, in order: the script's own directory
first, then any PYTHONPATH entries, then the standard library, then site-packages.
First in that list is the cause of the classic beginner trap. Name a file random.py or
json.py and your file wins over the standard library one — every import of it, anywhere in
the program, now gets yours. The symptom is a baffling AttributeError on a function you
know exists. Do not name files after standard library modules.
Packages
bank/
__init__.py # marks the directory as a package
money.py
models.py
stores.py
services.py
Then from bank.money import format_money, or from bank import money. The
dots follow the directories.
__init__.py runs when the package is first imported and is usually empty. Its other use
is deciding the package's public surface — putting from .money import format_money in it
lets callers write from bank import format_money without knowing which file it lives in.
Inside a package, use relative imports: from .money import format_money means "from the
money module beside me". One dot is this package, two dots is the parent. They make the package movable
and renameable without editing every file in it.
The __main__ guard
from pathlib import Path
Path("report.py").write_text(
'def build():\n'
' return "report"\n'
'\n'
'print("module-level code runs on import")\n'
'\n'
'if __name__ == "__main__":\n'
' print("only when run directly")\n',
encoding="utf-8",
)
import report
print(report.build())
# Output: module-level code runs on import
# Output: report
__name__ is "__main__" when the file is run directly and the module's own
name when it is imported. So the guarded block ran nowhere above — the file was imported, not run.
Without the guard, every module's top-level code fires the instant anyone imports it. That is how a utility module ends up starting a web server because someone wanted one function out of it. Put anything that does something behind the guard; leave definitions outside it.
Circular imports
Two modules that import each other. Python starts loading a, hits
import b, starts loading b, which hits import a — and gets the
half-finished a from the cache, missing whatever had not been defined yet. The error is
ImportError: cannot import name 'x' from partially initialized module.
There are three fixes and only one is good:
- Extract the shared thing. Whatever both modules need goes in a third module they both import. This is the real fix, because a cycle almost always means a missing module.
- Import inside the function rather than at the top. Works, and hides the dependency somewhere nobody reads.
- Import the module, not the name —
import bthenb.thing(), which defers the lookup until call time.
A circular import is a design problem wearing a syntax error's clothing. Reach for the first fix.
Installing packages
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt # record exact versions
pip install -r requirements.txt # reproduce them elsewhere
Always inside a virtual environment, one per project. Installing globally means two projects that need different versions of the same library will eventually fight, and the loser is whichever you touched last.
pip freeze pins exact versions, which is what makes an environment reproducible on
someone else's machine. Commit requirements.txt; never commit .venv/.
Laying out a small project
myproject/
bank/ # the package — your code
__init__.py
money.py
tests/ # mirrors the package
test_money.py
requirements.txt
README.md
Run it as python3 -m bank rather than python3 bank/__main__.py — the
-m form sets up the package correctly, so relative imports work. Running a file inside a
package directly is the other common source of "attempted relative import with no known parent
package".
Next: Async & Await.