Modules, imports, and testing with pytest
What import really does, why from-imports and mocks interact the way they do, circular imports, and the pytest features every interview assumes.
After this lesson you can
- Explain module caching in sys.modules and what from x import y binds
- Diagnose a circular import from its error message
- Patch a dependency where it is looked up, not where it is defined
- Write pytest tests with fixtures, parametrize, raises and approx
What import does
The first import settings finds settings.py on sys.path, creates
a module object, runs the file top to bottom to fill it, and stores
it in sys.modules. Every later import settings, anywhere in the
program, finds it there and runs nothing. A module is a singleton
namespace; module-level code is one-time setup.
The two import forms bind different things:
import settingsbinds the namesettingsto the module object.settings.LEVELis looked up on every use, so it sees changes.from settings import LEVELbinds a nameLEVELin your own namespace to the objectsettings.LEVELrefers to at that moment, once. If the module's attribute is rebound later, yourLEVELstill refers to the old object.
__name__ is the module's name when imported and "__main__" when the
file is run as a script, which is what if __name__ == "__main__":
checks. A package is a directory of modules, usually with an
__init__.py that runs when the package is imported; inside it,
from . import sibling is a relative import.
import importlibimport osimport sysimport tempfile def write(folder, name, source): with open(os.path.join(folder, name), "w") as f: f.write(source) def run(): folder = tempfile.mkdtemp() log = os.path.join(folder, "log.txt") write(folder, "settings.py", f"with open({log!r}, 'a') as f:\n" " f.write('executed ')\n" "LEVEL = 'info'\n") write(folder, "a.py", "from b import helper\nVALUE = 1\n") write(folder, "b.py", "from a import VALUE\n\ndef helper():\n return VALUE\n") sys.path.insert(0, folder) try: import settings import settings as again from settings import LEVEL settings.LEVEL = "debug" level_seen_by_module = settings.LEVEL with open(log) as f: before_reload = len(f.read().split()) importlib.reload(settings) with open(log) as f: after_reload = len(f.read().split()) try: import a circular = "imported" except ImportError as e: circular = f"ImportError: {str(e).split(' (/')[0]}" return { "same_object": settings is again, "executions_after_two_imports": before_reload, "executions_after_reload": after_reload, "module_LEVEL": level_seen_by_module, "from_imported_LEVEL": LEVEL, "module_name": settings.__name__, "circular": circular, } finally: sys.path.remove(folder) for name in ("settings", "a", "b"): sys.modules.pop(name, None)A circular import — a imports b while b imports a — leaves
one of them half-run when the other asks for a name, and fails with
"cannot import name ... from partially initialized module" (shown in
circular above). The fixes are structural: move the shared piece into
a third module, or import inside the function that needs it.
Testing with pytest
pytest (a third-party package; the snippet below is an illustration,
not run here) collects files named test_*.py or *_test.py and, in
them, functions whose names start with test. Tests use the plain
assert statement: pytest rewrites asserts in test modules so a
failure reports the values involved.
import pytest
from shop.pricing import total
@pytest.fixture
def cart():
items = [("tea", 2.5), ("cake", 4.0)] # setup
yield items
items.clear() # teardown, after the test
def test_total(cart): # fixtures arrive by name
assert total(cart) == pytest.approx(6.5)
@pytest.mark.parametrize("raw, expected", [("10", 10), (" 7 ", 7)])
def test_parse(raw, expected):
assert int(raw) == expected
def test_rejects_text():
with pytest.raises(ValueError, match="invalid literal"):
int("ten")
pytest.raises passes when the block raises that type or a
subclass, and match= is a regular expression searched for in the
exception's message. pytest.approx exists because binary floats
rarely add up exactly: is false (the second example
below shows it, with the standard library's as the
tolerant comparison). A fixture runs once per test by default;
or (also ,
) shares one instance more widely, which is why a fixture
that hands out mutable state is usually left at the default scope.
import mathimport osimport sysimport tempfilefrom unittest import mock def write(folder, name, source): with open(os.path.join(folder, name), "w") as f: f.write(source) def run(): folder = tempfile.mkdtemp() write(folder, "pricing.py", "def rate():\n return 0.2\n") write(folder, "checkout.py", "from pricing import rate\n\n" "def total(net):\n return round(net * (1 + rate()), 2)\n") sys.path.insert(0, folder) try: import checkout real = checkout.total(100) with mock.patch("pricing.rate", return_value=0.5): patched_definition = checkout.total(100) with mock.patch("checkout.rate", return_value=0.5): patched_lookup = checkout.total(100) return { "real": real, "patched_pricing_rate": patched_definition, "patched_checkout_rate": patched_lookup, "float_equal": 0.1 + 0.2 == 0.3, "float_close": math.isclose(0.1 + 0.2, 0.3), } finally: sys.path.remove(folder) for name in ("pricing", "checkout"): sys.modules.pop(name, None)Common mistakes
- Patching the module that defines a function when the code under test
imported it with
from ... import. The test runs against the real one. - Putting work with side effects (connecting, reading files) at module level. It runs on first import, including when tests import the module.
- Comparing computed floats with
==in a test instead ofpytest.approx. - Expecting a second
importto re-run a module after editing it. It is cached;importlib.reloadre-runs it, and existing from-imports still hold the old objects.
What interviewers ask
- What happens when you import a module twice? It runs once and is
cached in
sys.modules; the second import is a dictionary lookup. - How do you fix a circular import? Extract the shared code into a module both can import, or defer one import into a function.
- Where do you patch a dependency in a test? Where the code under test looks it up: the importing module's namespace.
- What is a pytest fixture? A function whose result is injected into
tests by parameter name, with setup before
yield, teardown after, and a scope that decides how often it runs.
Exercise · Pro
Which value does each name see?
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade