INW005 pure-domain¶
What it does¶
Reports an import, in a file in a layer, of a library the layer may not use. Each import is sorted into first-party (a layer or the project owns it, left to INW001 and INW006), standard library (a bundled list of CPython 3.11 to 3.14 module names) or third-party. A library import is then checked against three optional layer keys:
allow-libraries: once set, the layer may import only these third-party libraries. The standard library stays allowed.deny-libraries: the layer may not import these, standard library included. On the innermost layer it replaces the default deny list.extend-deny-libraries: adds to the layer's deny list, the default one included.
[tool.inwards.rules.pure-domain] can also deny libraries to module prefixes that aren't whole layers, with deny (#219): the modules an entry matches may not import its libraries, inside a layer or outside every layer, whatever the layer's allow-libraries says. The message then names the prefix: Module "mypackage.one.views" imports "django.db" from library "django", which [tool.inwards.rules.pure-domain] denies to "mypackage.one".
Without any of them, the innermost layer of a config with two or more layers denies a default list: frameworks (django, fastapi, flask...), databases and ORMs (sqlalchemy, psycopg, redis, sqlite3...), network clients (requests, httpx, boto3...) and stdlib I/O (socket, subprocess, http.client, urllib.request...). Other layers may import any library. The import is checked wherever it is, as for INW001, including literal dynamic imports (importlib.import_module("requests")). Libraries per layer has the full list and the matching rules.
Why is this bad¶
INW001 only looks at your own code, so from sqlalchemy.orm import Session in the domain passes it, even though it ties the domain to a database as firmly as import shop.infrastructure.db would. For an agent told not to import the infrastructure layer, importing the library directly is the obvious next move. It is also the most common leak in layered Python code written by people.
Example¶
With the layers from INW001, the domain uses an ORM session:
from sqlalchemy.orm import Session
class Order:
def __init__(self, session: Session, order_id: int) -> None:
self.session = session
self.order_id = order_id
shop/domain/order.py:1:28: INW005 Layer "domain" imports "sqlalchemy.orm.Session" from library "sqlalchemy", which "domain" may not use.
fix: Use "sqlalchemy" in an outer layer, behind a port owned by "domain".
1. Delete `from sqlalchemy.orm import Session`. Do not move the import into a function, behind TYPE_CHECKING or into importlib; Inwards checks those too.
2. Declare a typing.Protocol in `shop.domain` (for example `shop.domain.ports`) that describes only what this module needs from "sqlalchemy".
3. Type this module against that Protocol and receive the implementation through a constructor or function parameter.
4. Implement the Protocol with "sqlalchemy" in the outer layer that holds adapters (allowed: "application", "infrastructure"), and wire it in the outermost layer (the composition root).
5. If "domain" should be allowed to use "sqlalchemy", ask the user to add "sqlalchemy" to that layer's allow-libraries in [tool.inwards]. Don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW005/
Fixed: the domain declares what it needs, and the infrastructure implements it with SQLAlchemy.
from typing import Protocol
class Orders(Protocol):
def total(self, order_id: int) -> int: ...
from shop.domain.ports import Orders
def total(orders: Orders, order_id: int) -> int:
return orders.total(order_id)
from sqlalchemy.orm import Session
class SqlOrders:
def __init__(self, session: Session) -> None:
self.session = session
def total(self, order_id: int) -> int:
return 0
How to fix¶
- Delete the import. Moving it into a function, behind
TYPE_CHECKINGor intoimportlibonly moves the finding. - Declare a
typing.Protocolin the layer that describes only what the module needs from the library. - Type the module against the Protocol and take the implementation as a parameter.
- Implement the Protocol with the library in an outer layer that may use it. The fix lists every such layer; pick the one that holds adapters, which in a hexagonal layout isn't always the next one out.
- If the layer should be allowed to use the library, that is the user's decision: they add it to
allow-libraries. When no outer layer may use it either, the fix asks the agent to ask the user where the library belongs.
Workspace packages¶
In a uv workspace with one config per member, a sibling member's package is outside the config's root, so INW005 checks it like a library. When the import's top-level package belongs to a member of the nearest [tool.uv.workspace] at or above the config, the message says "workspace package" instead of "library": it is the team's own code, not a dependency to drop.
src/qv_datasources/core/x.py:1:8: INW005 Layer "core" imports "qv_core.models" from workspace package "qv_core", which "core" may not use.
fix: "core" may not use workspace package "qv_core": use a package it may use, or ask the user to allow "qv_core".
The fix has no port to write: use a package or workspace member the layer may use, or ask the user to add the package to allow-libraries, which covers workspace packages too. A member's packages are its [project] name as an import name, the directories under its src/, and, without src/, the directories next to its pyproject.toml that hold an __init__.py. Member globs match * inside one path segment only (packages/*); ** isn't supported. The VS Code extension doesn't look for the workspace yet and still says "library".
Fix safety¶
No automatic fix (autofix: false). Moving a library behind a port changes the design and needs review.
Configuration¶
Severity error. Per layer, in [tool.inwards]:
[tool.inwards]
layers = [
{ name = "domain", modules = ["shop.domain"], allow-libraries = ["attrs"], extend-deny-libraries = ["os"] },
{ name = "application", modules = ["shop.application"], deny-libraries = ["sqlalchemy", "requests"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
To deny a library to part of a layer, or to a package no layer owns:
[tool.inwards.rules.pure-domain]
deny = [{ modules = ["shop.domain.pricing"], libraries = ["numpy"] }]
Entries are import names (yaml, not PyYAML) that cover their submodules. When the allow and deny lists both match, the longer entry wins, and allow wins a tie. deny-libraries = [] on the innermost layer turns the default off. [tool.inwards.rules] can turn the rule off or make it a warning (severity = { INW005 = "warning" }), see Configure rules. One import can be suppressed on its line with # inwards: ignore[INW005] reason="...".
Known limitations¶
- Distribution names that differ from the import name aren't mapped: list
yaml, notPyYAML. - The standard-library list is fixed at build time, so a module added in a later CPython counts as third-party until Inwards updates the list.
- Adding the rule to an existing config with two or more layers can report libraries the domain uses on purpose.
deny-libraries = [], anallow-librariesentry orinwards baselineaccepts them.