Hello again. In the previous lesson, you shaped the incident domain with IncidentTitle and Incident dataclasses, keeping them independent from future HTTP and database representations. That separation only stays useful if the modules containing those types can be imported predictably.
This lesson completes an important part of the project foundation: organizing the incident_api package so its dependency direction is clear, imports do not form cycles, and importing a domain module does not unexpectedly run operational code. These choices make unit tests, CLI tools, future FastAPI startup, and CI checks considerably less surprising.
Imports are execution, not just declarations
In Python, the first import of a module does more than make names available. Python creates a module object, places it in its module cache, and executes the file’s top-level statements from top to bottom. Nested imports pause the current module while Python initializes the dependency.
That behavior explains two practical rules:
- A module should primarily define classes, functions, and constants.
- Code that performs work should have an explicit entry point rather than running merely because another module imported it.
Watch this short explanation from mCoding before examining the project structure. It traces the initialization sequence that produces the familiar “partially initialized module” error.
Avoiding import loops in Python
Watch “Avoiding import loops in Python” by mCoding for a concise visual model of how import cycles occur and the main ways to address them.
Watch the runtime loop to see how Python pauses one module while loading another and why the traceback identifies a partially initialized module. Continue with three repairs, focusing on the distinction between importing a module and importing a particular name. Finally, watch type hint cycles for the special case where otherwise useful annotations create a runtime dependency.
A top-level print() is an obvious import-time side effect. More harmful examples in a service codebase are less visible:
- opening a network connection or creating cloud clients;
- reading files or calling
os.makedirs(); - configuring global logging;
- starting background threads;
- parsing environment configuration into a global object that tests cannot change later;
- calling a function to seed data or make an API request.
Some top-level evaluation is normal and safe. A function definition, class definition, enum declaration, and @dataclass declaration belong there. The default_factory=uuid4 used in the previous lesson is also safe: it stores the callable at import time but does not generate a UUID until an Incident is constructed.
The key question is: could another module import this file during a test, type-checking run, or application startup without causing operational work? For most library and domain modules, the desired answer is yes.
Give the package a simple, one-way dependency shape
You already created an installable project with a src layout. For now, there is no need to split every concept into a subpackage. Keep the two modules from the previous lessons, and make their relationship explicit:
src/
└── incident_api/
├── __init__.py
├── incident_enums.py
├── domain_models.py
└── cli.py
__init__.py makes incident_api an ordinary package in the conventional sense. Modern Python can support namespace packages without it, but an explicit, initially minimal __init__.py is a clear choice for an application package.
The intended dependency policy is:
| Module | May import | Must not import |
|---|---|---|
incident_enums.py | Standard-library modules only, if needed | Domain models, CLI code, future API or database code |
domain_models.py | incident_enums.py, standard library | CLI code, routers, repositories, framework code |
cli.py | Domain modules and presentation helpers | Nothing should import the CLI as a reusable dependency |
This is a dependency hierarchy, not merely a folder arrangement. The lowest-level module knows the least about the application.
For the code currently written in the course, use package-qualified absolute imports:
# src/incident_api/domain_models.py
from dataclasses import dataclass, field
from datetime import datetime, timezone
from uuid import UUID, uuid4
from incident_api.incident_enums import IncidentSeverity, IncidentStatus
The import states exactly where the project type comes from. It also works consistently when the project is installed in editable mode, run in CI, or imported from a separate tool.
Python supports relative imports too:
from .incident_enums import IncidentSeverity, IncidentStatus
This is valid inside a package. However, relative imports depend on Python knowing the module’s package context. For this project, consistently using absolute imports under incident_api makes the boundary visible and avoids confusion about whether a name refers to a local file or an unrelated installed package.
The following selected FAQ material reinforces the usual import ordering, the mechanics of circular imports, and the role of __name__.
Programming FAQ — Python 3.14.7 documentation
Read the official Python Programming FAQ for the language-level rules behind the import conventions used in this lesson.
In “What are the best practices for using import in a module?”, read the import-practice guidance. Focus on the standard-library, third-party, and local-project ordering, and on why a local import is an exception rather than a default. Then, in “How can I have modules that mutually import each other?”, read the initialization sequence. Follow the point at which Python tries to obtain a name from a module whose top-level execution has not yet completed. Finally, in “How do I find the current module name?”, read the paragraph beginning the main-module explanation, then inspect the short main() example immediately below it.
A restrained __init__.py helps preserve this shape:
# src/incident_api/__init__.py
"""Incident management API package."""
Avoid automatically re-exporting every internal class from __init__.py:
# Avoid as a default in an application package
from incident_api.domain_models import Incident
from incident_api.incident_enums import IncidentSeverity
Re-exports can be appropriate for a deliberately designed public library interface. Within an application, though, they make package initialization load more modules than the caller requested. This can obscure dependencies and can accidentally create cycles as the package grows. Import the specific module that owns the concept instead.
Why circular imports fail
A circular import is not simply “two files mentioning each other.” It becomes a real problem when one module needs a name from another before that other module has finished its own initialization.

Consider this unhealthy direction in the incident project:
# src/incident_api/cli.py
from incident_api.domain_models import Incident
def format_incident(incident: Incident) -> str:
return f"{incident.incident_id}: {incident.title.value}"
# src/incident_api/domain_models.py
from incident_api.cli import format_incident # Wrong dependency direction
If Python begins importing cli.py, it must first import domain_models.py. That module then tries to import format_incident from cli.py, but cli.py has not yet reached the function definition. Python therefore raises an error similar to:
ImportError: cannot import name 'format_incident' from partially initialized module
The immediate fix is not to shuffle import lines until the error disappears. The deeper defect is that a domain module depends on an outer operational interface. A domain model should not need to know how a terminal formats it.
Instead, extract the formatting concern into a module that depends on the domain, never the reverse:
# src/incident_api/incident_formatting.py
from incident_api.domain_models import Incident
def format_incident(incident: Incident) -> str:
return f"{incident.incident_id}: {incident.title.value}"
# src/incident_api/cli.py
from incident_api.domain_models import Incident
from incident_api.incident_formatting import format_incident
# src/incident_api/domain_models.py
from incident_api.incident_enums import IncidentSeverity, IncidentStatus
# Domain dataclasses only; no CLI or formatting imports.
Now the formatting code may know about Incident; the Incident model does not know about formatting. The cycle is gone because the design has a clear ownership direction.
When diagnosing a circular import, use this order of remedies:
-
Correct the dependency direction.
Move a concern outward, or extract shared code into a third module that both original modules can import. -
Merge modules that truly represent one inseparable unit.
Two tiny modules that must always know each other may have been split too early. -
Use a local import only for a narrow, unavoidable runtime dependency.
Python permits imports inside functions. It defers the import until the function is called, after normal module initialization has completed. -
Use type-checking-only imports when the dependency exists solely for static annotations.
The local-import technique is useful but should not conceal a design problem:
def render_for_terminal(incident_id: str) -> str:
from incident_api.incident_formatting import format_incident
incident = load_incident(incident_id)
return format_incident(incident)
This is reasonable if incident_formatting is needed only by this rare path, or if there is a carefully understood import-time constraint. It is not a good reason to make every dependency local. Top-level imports remain the clearest declaration of a module’s normal requirements.
There is also a subtle distinction between these two forms:
import incident_api.incident_formatting
from incident_api.incident_formatting import format_incident
The second form asks Python to obtain format_incident immediately. The first obtains the module object; accessing incident_api.incident_formatting.format_incident can happen later. This can make a particular cycle appear to work, but it does not repair a bad dependency graph. Prefer redesign over relying on initialization order.
Keep executable code at an explicit entry point
A CLI is operational code: it parses arguments, selects an action, prints output, and returns an exit status. It should be runnable without making those actions occur whenever another module imports cli.
# src/incident_api/cli.py
from incident_api.domain_models import Incident, IncidentTitle
from incident_api.incident_enums import IncidentSeverity
from incident_api.incident_formatting import format_incident
def main() -> int:
incident = Incident(
title=IncidentTitle(value="Checkout unavailable"),
severity=IncidentSeverity.CRITICAL,
)
print(format_incident(incident))
return 0
if __name__ == "__main__":
raise SystemExit(main())
When Python imports incident_api.cli, its __name__ is incident_api.cli, so the guarded block does not run. The module contributes a callable main() but does not print anything or create an incident.
When executed as a module, Python sets the executed module’s __name__ to "__main__", so the guard runs:
python -m incident_api.cli
For the src layout introduced earlier, this is preferable to running the source file directly:
# Avoid
python src/incident_api/cli.py
The direct-file command makes the file’s own directory the starting import location, which can prevent incident_api from being found as a package. It also encourages imports that work only from one working directory. Running with -m preserves package context; installing the project in editable mode makes the package available in the active virtual environment.
The same principle will matter when FastAPI arrives. Importing application modules should construct only the objects required by the ASGI server’s contract, not start servers, run migrations, contact dependencies, or execute application workflows. Startup and shutdown work will later receive dedicated lifecycle boundaries.
Type hints can create a different import cycle
Type annotations are valuable, but Python normally needs names in annotations to be available when a function or class is defined. For a type used only by a static checker, avoid importing it at runtime:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from incident_api.assignments import Assignment
def assignment_label(assignment: Assignment) -> str:
return assignment.assignee_name
TYPE_CHECKING is False while the program runs, but static type checkers treat its contents as available. from __future__ import annotations ensures the annotation is not immediately evaluated as the runtime name Assignment.
Use this technique only when the type is genuinely unnecessary at runtime. If code performs runtime type inspection, calls typing.get_type_hints(), or a framework must resolve the annotation itself, hiding the import may simply turn an import cycle into a later runtime failure. In that case, return to the first remedy: improve the dependency structure.
Implementation checkpoint
Refactor the current project into this import-safe baseline:
- Keep
incident_enums.pyindependent of your own project modules. - In
domain_models.py, import enums throughincident_api.incident_enums. - Add a small
cli.pywith amain() -> intfunction and anif __name__ == "__main__":guard. - Ensure neither
domain_models.pynorincident_enums.pyprints, reads configuration, creates directories, or contacts services at import time. - Keep
incident_api/__init__.pyempty except for a docstring or similarly harmless package metadata. - Run these lightweight smoke checks from the project root with the virtual environment active:
python -c "import incident_api.domain_models"
python -m incident_api.cli
The first command should finish silently. The second may produce the intentional CLI output. If the first command prints output, creates files, requires configuration, or fails because of an unrelated operational dependency, treat that as an import-boundary defect.
Key takeaways
Imports execute module-level code the first time a module is loaded. Therefore, importable modules should be safe to load and should primarily define functionality rather than perform work.
- Organize dependencies from outer operational code toward stable domain code, never the reverse.
- Use package-qualified absolute imports consistently in this installed
src-layout project. - Keep
__init__.pyminimal unless you have a deliberate public-library interface to expose. - Treat a circular import as a design signal first; solve it by changing ownership or extracting shared code before resorting to local imports.
- Put runnable behavior in
main()and protect it withif __name__ == "__main__":. - Use
TYPE_CHECKINGplus postponed annotations only for dependencies needed exclusively by static analysis.
Next, you will configure Ruff to format the project and enforce a consistent set of linting rules—turning several of these import and style conventions into automated feedback during local development and CI.
Can't find a good explanation? Sign up and we'll make it for you
Sign up