Domain Types Linter
A static code analyzer that enforces the use of domain-specific types in business logic code instead of universal types.
Description
Domain Types Linter is a tool that helps maintain better code organization and type safety by ensuring that your business logic layer uses only explicit business logic objects, not universal types. It analyzes Python type annotations and reports violations of domain type rules.
The linter detects three types of problems:
- Using an alias for a universal type (e.g.,
alias_str = str) - Using a universal base type directly (e.g.,
str,int) - Using a generic type without domain-specific parameters (e.g.,
Listwithout proper domain types)
Installation
Requirements
- Python 3.10 or higher
Basic Installation
pip install domain-types-linter
Installation with Flake8 Support
pip install domain-types-linter[flake8]
Usage
Command Line Interface
You can run the linter directly from the command line:
dt-linter path/to/file_or_directory
The linter will scan the specified file or directory recursively and report any domain type violations
in annotations of functions (including async functions), class attributes and variables.
It will exit with code 1 if problems are found.
Flake8 Plugin
If you have installed the linter with Flake8 support, you can use it as a Flake8 plugin:
flake8 path/to/file_or_directory
The domain type violations will be reported along with other Flake8 errors.
Configuration
Keeping domain types for every function is expensive. A helper that is used in one module
rarely needs its own domain types. The rules can be relaxed in the [tool.domain-types-linter]
section of pyproject.toml. Both the CLI and the Flake8 plugin use the closest pyproject.toml
with this section. By default, all checks are enabled.
[tool.domain-types-linter]
# Check only these paths (relative to the directory of pyproject.toml).
include = ["src/app/domain", "src/app/services"]
# Skip these paths. "*" also matches "/", e.g. "*/migrations/*".
exclude = ["src/app/services/legacy", "*/migrations/*"]
# Types that are allowed everywhere.
allowed-types = ["bool", "Any"]
# Check functions, methods and classes whose names start with "_" (except dunder methods like __init__).
check-private = false
# Check functions and classes defined inside other functions.
check-nested = false
# Check annotations of local variables inside functions (class and module attributes are always checked).
check-local-variables = false
# Frequent mode, see below (CLI only).
frequent = false
min-occurrences = 3
min-modules = 1
A pattern in include and exclude matches a file if it is the file path, one of its parent
directories, or a glob that matches the path.
Use --config path/to/pyproject.toml to point the CLI to a specific config file.
Frequent mode
A domain type is worth creating for a concept that is used in many places.
In frequent mode the linter reports only names of parameters, attributes and variables
that are annotated with universal types at least min-occurrences times
in at least min-modules modules:
dt-linter src --frequent --min-occurrences 3 --min-modules 2
src/users.py:4: DT004 forbidden to use universal type 'int' ('user_id' is annotated with universal types 14 times in 6 module(s), consider a domain type)
Names are compared without leading underscores, so _user_id and user_id are counted together.
Return annotations have no name and are not reported in this mode.
Frequent mode needs the whole project, so it is available in the CLI only and is ignored by the Flake8 plugin.
Suppressing problems
# noqaor# noqa: DT004at the end of a line suppresses problems on that line (in the CLI; Flake8 handlesnoqacomments itself).# dt: ignoreon the line of adeforclassskips the whole function or class, including its body. Works in the CLI and in the Flake8 plugin.
def parse_row(row: dict) -> tuple: # dt: ignore
...
def get_user(user_id: int, raw: str): ... # noqa: DT003
Examples
Disallowed Types
The following types are not allowed in business logic code:
# Universal base types
def disallowed_base_types(
my_str: str,
my_int: int,
my_float: float,
my_complex: complex,
my_bytes: bytes,
my_bytearray: bytearray,
my_any: Any,
my_decimal: Decimal,
):
...
# Aliases of universal types
alias_str = str
typing_type_alias_str: TypeAlias = str
def disallowed_aliases(
my_alias_str: alias_str,
my_typing_alias: typing_type_alias_str,
):
...
# Generic types with universal type parameters
def disallowed_generic_types(
my_list_str: list[str],
my_dict_str_int: Dict[str, int],
my_set_int: Set[int],
my_tuple_int: Tuple[int, ...],
my_optional_int: Optional[int],
):
...
# Generic types without parameters
def disallowed_generic_types_without_params(
my_list: list,
my_dict: dict,
my_set: set,
my_tuple: tuple,
my_iterable: Iterable,
):
...
Allowed Types
The following types are allowed in business logic code:
# Domain-specific types
UserId = NewType("UserId", int)
class DomainDataType:
...
# Using domain-specific types
def allowed_types(
my_bool: bool, # bool is allowed
my_none: None, # None is allowed
my_callable: Callable, # Callable is allowed without parameters
my_userid: UserId, # Domain-specific type
my_domain_type: DomainDataType, # Domain-specific class
):
...
# Generic types with domain-specific parameters
def allowed_generic_types(
my_list_userid: List[UserId],
my_dict_userid_domain: Dict[UserId, DomainDataType],
my_set_userid: Set[UserId],
my_tuple_userid: Tuple[UserId, ...],
my_optional_userid: Optional[UserId],
):
...
Problem Codes
The linter uses the following problem codes:
DT001: Using an alias for a universal typeDT003-DT011: Using universal base types (str, int, float, etc.)DT100-DT125: Using parameterized types without domain-specific parameters
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Metadata
Release files for domain-types-linter 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| domain_types_linter-1.1.0.tar.gz | 26.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| domain_types_linter-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.8 kB
Release files / domain_types_linter-1.1.0.tar.gz
| Download URL | domain_types_linter-1.1.0.tar.gz |
|---|---|
| Size | 26.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8453db11342f69395c9b57ce8dc7f42f12f585308560441df6d75c14fcf6245a
|
|
BLAKE2b-256 checksum How to use checksums |
0997f87c3c10a489e1f21e7263dba25a375c73a2d7558be445186522875ee1f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / domain_types_linter-1.1.0-py3-none-any.whl
| Download URL | domain_types_linter-1.1.0-py3-none-any.whl |
|---|---|
| Size | 14.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f7b5b2e810304994e7a6bf0826f34d0ec64f89c98ce92ff3bb8aced84880c57e
|
|
BLAKE2b-256 checksum How to use checksums |
ce42d72e3ac8c0589d7e9407b2b93fefed87908ac159dd14f4f916745c1a2a46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|