INW010 unknown-first-party¶
What it does¶
Reports a static import, in a file in a layer, of a first-party module that doesn't exist, and a relative import that climbs above the top-level package, which Python always refuses. An import is first-party when a prefix of it is a module of the project.
Only the module part is checked: X of from X import name, and the whole name of import X or from X import *. Existence is probed on disk the way Python imports, so namespace packages, stubs (.pyi) and compiled extensions (.so, .pyd, .pyx, .pyc) count. The fix lists the three modules of the same package closest to the missing name. Packages whose __init__.py extends __path__ (pkgutil.extend_path, declare_namespace) are skipped. Under an implicit namespace package (no __init__.py from the top-level package down), a module that another uv workspace member or the project's virtualenv holds counts as existing, since Python merges the package from every directory that has it (Monorepos and uv workspaces, installed namespace packages). A module that a generated pattern covers counts as existing, since a build step writes it.
An import INW010 reports, or accepts because another portion of a namespace package holds it, gets no INW006 as well, and an outward import INW001 reports gets no INW010: INW001's fix deletes the import, while INW010's would create the module.
Why is this bad¶
Agents invent plausible modules: from shop.domain.pricing import DiscountPolicy, where pricing doesn't exist. Without a check it surfaces as an ImportError at test time, if a test covers the file. INW010 flags it in the same hook run, before any test is written, and points at the near miss the agent probably meant.
Example¶
With the layers from INW001, and prices.py and money.py in the domain:
shop/domain/order.py:1:33: INW010 "shop.domain.pricing" is not a module of this project: "shop.domain" has no "pricing".
fix: Import a module that exists, or create "shop.domain.pricing" first.
1. Check the name. The closest modules in "shop.domain": `shop.domain.prices`, `shop.domain.money`.
2. If the code isn't written yet, create "shop.domain.pricing" before importing it.
3. Don't wrap the import in try/except ImportError; Inwards checks those too.
docs: https://sircypkowskyy.github.io/inwards/rules/INW010/
Fixed: the policy lives in prices.py.
How to fix¶
- Check the name against the closest modules the fix lists, and import the one that exists.
- If the code isn't written yet, create the module first, then import it.
- Don't wrap the import in
try/except ImportError: the rule checks it there too, and the code would still fail at runtime.
Fix safety¶
No automatic fix (autofix: false). The closest name by edit distance isn't always the module the code meant, so a person or the agent has to pick.
Configuration¶
Severity error. [tool.inwards.rules] can turn the rule off or make it a warning (Configure rules).
Generated modules¶
Some modules exist only after a build step: orders_pb2.py and orders_pb2_grpc.py from protoc, _version.py from setuptools-scm or hatch-vcs. A developer's checkout has them and a fresh CI checkout doesn't. INW010 treats a module that generated in [tool.inwards] covers as existing, file or no file (ADR-029):
- A pattern is a dotted module name, and each segment may use
*and?. It matches whole segments anywhere in the resolved module name, asignoredoes:*_pb2coversshop.api.orders_pb2,_versioncoversshop._version,shop.api.gencovers everything undershop/api/gen/. A wildcard never crosses a dot. - There are no bracket sets. An empty segment, a character that can't be in a module name (
[and]included) or a pattern made only of wildcards and dots (*,*.*) is a config error (exit 2). To turn INW010 off, useignore = ["INW010"]in[tool.inwards.rules]. - Without the key the list is
["*_pb2", "*_pb2_grpc", "_version"]. Setting the key replaces that default, so list the default patterns too if you still want them, andgenerated = []turns it off. - Only INW010 reads the key. The other rules still see a generated module that isn't on disk as missing: an outward import of one is still INW001, and INW006 names the nearest package that exists.
With the default, an import of a protoc module that isn't generated yet passes:
Any other import can be suppressed on its line, with a reason:
from shop.domain.legacy_rates import RATES # inwards: ignore[INW010] reason="copied in by the deploy script, see #230"
Namespace packages shared with installed distributions¶
An implicit namespace package (no __init__.py) can be split between the project and an installed distribution. Take a local acme/platform/billing/, with no __init__.py in acme/ or acme/platform/, and an installed acme-platform-auth that provides acme/platform/auth/. import acme.platform.auth.tokens works when the code runs, although auth isn't in the project.
INW010 doesn't skip every missing module under a namespace package: a made-up one would then pass. It needs evidence that the module lives elsewhere:
- The project's virtualenv. The CLI and the hooks look in the site-packages of the
.venvnext topyproject.tomland, in a uv workspace, at the workspace root (.venv/lib/python3.*/site-packages,.venv/Lib/site-packageson Windows). A module found there counts as existing. Nothing is run: no Python, no network. namespace-packagesin[tool.inwards], for runs without a virtualenv, such as CI beforeuv sync. A module directly inside a listed package that isn't in the project counts as existing, whatever its name.
Either way, the package the module would live in must be a namespace package all the way down from the top-level package, since Python merges only those. A missing module inside a subpackage the project has is still reported: acme.platform.billing.pricing is an error when acme/platform/billing/ has no pricing, with or without the key. A regular package (with __init__.py) isn't merged, so a missing module in one is reported too.
Known limitations¶
from shop.domain import pricingpasses whenshop/domainis a package, sincepricingcould be a name its__init__.pydefines (ADR-025).- A made-up import whose name a
generatedpattern covers passes INW010, by default any*_pb2,*_pb2_grpcor_versionmodule (shop.api.payments_pb2with nopayments.proto). It still fails when the code runs. To catch those, setgenerated = []and run the generator before the check (ADR-029). - An optional import behind
try/except ImportErroris reported. - Only a
.venvnext to the config or at the uv workspace root is searched. A virtualenv elsewhere (VIRTUAL_ENV, Poetry's cache, conda) and editable installs through.pthfiles aren't; list the package innamespace-packagesinstead. namespace-packagesaccepts any name directly inside a listed package, so a made-up sibling (acme.platform.authz) passes. Deeper names under a local subpackage are still checked.- Dynamic imports aren't checked for existence.
References¶
- ADR-025: INW010 probes the disk for existence and checks only the module part of an import
- ADR-029: Generated modules pass INW010, protoc and version modules by default
- Generated modules in CI
- Catching hallucinated modules