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/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
# Skip these functions and methods entirely, with their bodies (e.g. "to_dict", "__eq__").
exclude-functions = ["to_dict"]
# 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

  • # 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.0

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.0
File Size Uploaded
domain_types_linter-1.2.0.tar.gz 19.5 kB Details

Built distribution (wheel)

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

Total release size: 34.8 kB

Release files / domain_types_linter-1.2.0.tar.gz

Download URL domain_types_linter-1.2.0.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
deb5d2ad3125437aeefbf59bc90e45861e8a7ebb904f717dba25aafd1875582f
BLAKE2b-256 checksum
How to use checksums
ec4ad2010022abec0baa94cd860a4bfc062945789f8078627f77e7d106f5b370
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.0-py3-none-any.whl

Download URL domain_types_linter-1.2.0-py3-none-any.whl
Size 15.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79254c7cedf1f6b37964e1eb062dd615e6f23a2bf79d139338b3f6f657676124
BLAKE2b-256 checksum
How to use checksums
1d2a8762e7791d2d219a74150176637510b699e623a3300ce88e340118f0286b
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

1.2.1

2 release files

This release

1.2.0 This release

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