skip to content

Inside your own importable package, when do relative imports beat absolute ones?

level: seniorimportance: should knowfreq 38%

answer

  1. Ask what happens when the package moves
  2. One follows the package, one names the world
  3. Relocatable and vendored versus greppable
  4. Which form pins your own sibling copy
  5. Cap the dot depth at two

basics

~20 s

Use relative imports for references inside a package that may be renamed, nested or vendored: they follow the package wherever it goes and always pin the sibling that ships with it. Use absolute imports across packages and in entry-point modules.

solid answer

~40 s

A relative import computes its target from the importing module's own package name, so it always means "the module shipped beside me". That makes a package relocatable: rename it, nest it, or vendor it into another tree and its internals need no edits, and they can never bind to a same-named distribution that happens to be installed. An absolute import is a fixed coordinate — greppable, readable without knowing where the file lives, stable when a file moves between packages, and the only form that works in a module also used as an entry point. The usual convention is relative for siblings and children inside a package, absolute across top-level packages and in entry points, with dot depth capped at two. Neither form costs anything at runtime; both resolve to the same cached module.

code

python · 7 lines
python
from importlib.util import resolve_name

# 'from ..core import io' written in a module of app.services
print(resolve_name("..core.io", package="app.services"))        # app.core.io

# the identical line after the file moves one package deeper
print(resolve_name("..core.io", package="app.services.jobs"))   # app.services.core.io

go deeper

for a junior

Know that both forms exist and follow whatever the surrounding modules already do rather than mixing styles in one package. Be able to say that a relative import means the module shipped beside this one.

for a middle

Explain the mechanical consequence of each: a relative import follows the package when it is renamed or nested, an absolute import survives a file moving between packages and is searchable across the repository.

for a senior

Argue the choice from real failures: vendoring or re-namespacing a package whose internals are absolute can bind half the code to an installed copy of the old name, and moving a file changes what its dots mean.

for a principal

Own it as a repository-wide rule with lint enforcement, tie it to how packages are split into distributions and vendored, and define each package's public surface so callers never reach into its internals in either form.

### The property that actually separates them Both forms end up asking the machinery for the same kind of thing: an absolute dotted name. The difference is where that name comes from. An absolute import hard-codes it in the source line. A relative import computes it at import time from the importing module's own package name. That single difference produces every tradeoff below. **A relative import is self-referential.** `from . import segments` means "the `segments` module that ships beside me, in whatever package I currently am". Rename the top-level package, nest it inside another project, or vendor it into an application tree, and every internal reference follows automatically, because the package name it computes from moved with it. **An absolute import is a fixed coordinate.** `from tmcore.segments import MAX_LEN` names one module in the whole process, no matter which file the line appears in. It is greppable, unambiguous to a reader who has no idea where the file lives, unaffected by moving the file between packages, and it works in a module that is also used as an entry point — where relative imports fail outright for lack of a package name. ### The failure that teaches the distinction A translation-memory updater vendored a shared library into its own tree, re-namespacing it as `app.vendor.tmcore`. The library's internal references were absolute — `from tmcore.segments import MAX_LEN` — so they kept naming the *old* top-level, and an older `tmcore` release was still installed in the environment. Import resolution is by name, so those lines resolved to the installed copy while the surrounding modules came from the vendored one. Half the code ran against the new segment handling, half against the old constant. Nothing raised: the updater rebuilt a 2.4 GB working set and silently truncated every segment past the old limit, which surfaced days later as mangled translations rather than as a stack trace. Had the library's internal references been relative, the re-namespaced copy would have referred to itself and the second copy would never have been reachable at all. The general rule that follows: **for references inside a package, the relative form expresses the intent you actually have** — *my* sibling, this copy — and that intent is exactly what an absolute name cannot express, because absolute names are resolved against the environment. ### What relative imports cost They change meaning when a file moves. `from ..core import io` inside `app.services.loader` names `app.core.io`; move that file down into `app.services.jobs` and the same line now names `app.services.core.io` — which may not exist (a loud failure) or may exist as something else (a quiet one). Absolute imports break loudly on the old name instead, which is usually the failure you want during a refactor. They are unreadable past two dots. `from ....common.text import normalize` gives a reader no idea what package that is without counting the file's own depth, and it is a signal that the package is too deeply nested, not that the import form is wrong. They forbid the module being run as a top-level entry point, since a module loaded without a package name cannot resolve a dot at all. And they are harder to grep: searching for `tmcore.segments` finds absolute users but no relative ones. ### A convention that survives review The practice most large codebases converge on is a split by direction. **Within a package, use explicit relative imports for siblings and children**, so the package is relocatable and its internal graph is obvious. **Across top-level packages, use absolute imports**, so a cross-package dependency is visible at a glance and countable by a lint rule. **In modules that are also entry points, use absolute imports throughout**, since they must work when the module has no package. And cap the dot depth — one or two dots; anything deeper is a restructuring signal. The stronger the case for a package being copied, vendored, renamed or published under a different distribution name, the stronger the case for relative imports inside it. A leaf application that will never move can use absolute imports everywhere and lose nothing, and many teams prefer exactly that for its readability. Two things are worth stating precisely because candidates get them wrong. First, mixing both forms for the same target is a consistency problem, not a correctness one: both resolve to the same absolute name, so both land on the same entry in the module cache and there is no risk of loading the module twice. Second, neither form has any runtime cost difference — the resolution step for the dots is trivial string work, and after the first import both are a cache hit. The choice is entirely about what happens to your code when the package moves, when someone reads it, and when a same-named distribution shows up in the environment.

  • What does `from ...util import x` do inside the module `app.services.loader`?
    It raises `ImportError: attempted relative import beyond top-level package`. One dot is `app.services`, two is `app`, and the third counts past the top-level name with nothing left to join. The machinery never inspects the parent directory to see whether it might be a package — the whole computation is arithmetic on the package name.
  • Does mixing relative and absolute imports for the same module load it twice?
    No. Both forms resolve to the same absolute dotted name and therefore hit the same entry in the module cache, so there is exactly one module object. Mixing them is a readability and consistency problem worth a lint rule, not a correctness bug. Genuine double-loading comes from a module being reachable under two different names, such as `__main__` and its dotted name.
  • How would you enforce the convention across a large repository?
    Encode it as a lint rule rather than a style-guide sentence: forbid relative imports that cross a top-level package boundary, cap dot depth, and require absolute imports in modules registered as entry points. Then keep the public surface of each package as explicit re-exports in its top-level module, so callers depend on one name that the package controls.

saying these in an interview costs you the question

  • Claims relative imports are deprecated in Python 3
  • Says one form is faster or cheaper at runtime
  • Assumes an absolute import always finds the copy shipped alongside
  • Uses four or five dots without seeing a nesting problem
  • Treats the choice as purely cosmetic
  • Thinks mixing both forms loads the module twice

context