Skip to main content

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., List without 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/first path/to/second

The linter will scan the specified files or directories recursively and report any domain type violations in annotations of functions (including async functions), class attributes and variables. Messages name the annotated parameter, attribute or variable when it has one. 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
# Skip these functions and methods entirely, with their bodies (e.g. "to_dict", "__eq__").
exclude-functions = ["to_dict"]
# Ignore known problems listed in this file (see below).
baseline = "baseline.txt"
# Frequent mode, see below (CLI only).
frequent = false
min-occurrences = 3
min-modules = 1

Baseline

The baseline file lists known problems to ignore, one per line:

# filepath relative to the config root, line "0" matches any line
src/users.py:12: DT004
src/orders.py:0: DT003

Pass it with --baseline path/to/baseline.txt or set baseline in the config.

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

  • # noqa or # noqa: DT004 at the end of a line suppresses problems on that line (in the CLI; Flake8 handles noqa comments itself).
  • # dt: ignore on the line of a def or class skips the whole function or class, including its body. Works in the CLI and in the Flake8 plugin.
  • Dunders with protocol-fixed signatures returning universal types are skipped by default (__hash__, __repr__, __str__, numeric ones like __int__/__len__, etc.). Add more names with exclude-functions (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 type
  • DT003 - 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.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for domain-types-linter 1.2.1
File Size Uploaded
domain_types_linter-1.2.1.tar.gz 20.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for domain-types-linter 1.2.1
File Interpreter ABI Platform
domain_types_linter-1.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 37.2 kB

Release files / domain_types_linter-1.2.1.tar.gz

Download URL domain_types_linter-1.2.1.tar.gz
Size 20.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ac16179bd5978212f7a870bf0bb1777f66ef14cda43321f7a68407e76cdb0c52
BLAKE2b-256 checksum
How to use checksums
0c104a66e3449ecbe3660702ab16d8fdf28ecea0eeacec323ad427238f1a9609
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.2.1-py3-none-any.whl

Download URL domain_types_linter-1.2.1-py3-none-any.whl
Size 16.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3d6c65cbf4d2e42317d7a49b27fe7dc1e174a48393380c04ad330f9c520dca9
BLAKE2b-256 checksum
How to use checksums
1ff9dbf2424c8c0af217ece7fc3967086cc2e8bbd24accb5040d442cb14bfded
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 history Release notifications | RSS feed

This release

1.2.1 This release

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page