Skip to main content

sergey

A Python linter with opinionated rules about import style, naming, and code structure. Runs as a CLI tool or as an LSP server for editor integration.

The primary intent of Sergey is to enforce my personal stylistic rules upon agentic code. However, you may also find these useful in standard development. Simultaneously, it is a testing space for me for agentic coding.

Installation

uv add sergey

Usage

CLI

sergey check path/to/file.py     # check a single file
sergey check src/ tests/         # check directories (recursive)
sergey check .                   # check the whole project

Exits with code 0 if no violations are found, 1 if any are found.

LSP server

sergey serve

Communicates over stdio using the Language Server Protocol. Configure your editor to launch this command as a language server for Python files.

Rules

Imports

Rule Description
IMP001 from module import name is disallowed when name is not itself a submodule. Use import module and reference module.name at call sites. Typing modules, __future__, and collections.abc are exempt (see IMP002 and IMP004).
IMP002 import typing and import typing_extensions are disallowed. Use from typing import X and from typing_extensions import X to import names directly.
IMP003 Dotted plain imports (import os.path) are disallowed. Use from os import path instead. collections.abc is exempt (see IMP004).
IMP004 import collections.abc is disallowed. Use from collections.abc import X to import names directly.

The four rules together enforce a consistent import style: every name you use is either a bare module you imported at the top level, or a submodule you accessed via from package import submodule.

Naming

Rule Description
NAM001 Functions annotated -> bool must start with a predicate prefix: is_, has_, can_, should_, will_, did_, or was_. Dunder methods are exempt. Leading underscores on private helpers are ignored (_is_valid passes).
NAM002 Single-character variable names are disallowed in assignments, for-loops, comprehensions, with-statements, and walrus expressions. The conventional throwaway _ is exempt.
NAM003 Single-character function and method parameter names are disallowed. Covers positional-only, regular, and keyword-only parameters. _, *args, and **kwargs are exempt. Lambda parameters are not checked.

Documentation

Rule Description
DOC001 Functions that contain explicit raise statements must include a Raises section in their docstring. Bare re-raises (raise with no argument) are exempt. Raises inside nested functions or classes belong to those scopes and are not counted against the outer function. Functions with no docstring are not checked. Both Google style (Raises:) and NumPy style (Raises / ------) are accepted.

Pydantic

Rule Description
PDT001 Every BaseModel subclass must have model_config = ConfigDict(frozen=...) with frozen explicitly set. This forces a deliberate decision about mutability. Both frozen=True and frozen=False are accepted; omitting frozen or omitting model_config entirely is flagged.
PDT002 Frozen BaseModel subclasses (frozen=True) must not have fields annotated with mutable types such as list, dict, set, deque, etc. Use immutable alternatives (tuple, frozenset, …) instead. The check recurses into generic parameters and union syntax, so Optional[list[str]] and str | list[int] are both caught. ClassVar annotations are exempt.
PDT003 Fields in non-frozen BaseModel subclasses (frozen=False) must each declare their own Field(frozen=True) or Field(frozen=False). This forces a deliberate per-field immutability decision rather than silently inheriting the model-wide mutable default. The Field(frozen=...) may appear as the default value or inside an Annotated annotation. ClassVar fields and model_config are exempt.

Structure

Rule Description
STR002 Control-flow blocks nested deeper than 4 levels are flagged. Counted constructs: if/elif/else, for, while, with, try, match. elif branches count at the same depth as their leading if. Function, class, and lambda definitions reset the counter, so nested functions are judged independently.
STR003 try bodies containing more than 4 statements are flagged. Statements are counted recursively (an if with branches contributes 1 plus all contained statements). Only the try: body is counted — except and finally blocks are not subject to this rule. Nested functions and classes reset the count.
STR004 List and set literals inside functions that are never mutated and are not part of the function output (return/yield) should use immutable alternatives: tuple instead of [] and frozenset instead of {}. Only plain literals are checked; constructor calls and comprehensions are not covered.
STR005 Module-level constants (SCREAMING_SNAKE_CASE names) must carry a Final annotation (name: Final = ...) so static type checkers can enforce that they are never reassigned. Plain assignments and non-Final annotations are flagged. Dunder names (__all__, __version__, etc.) are exempt. Only direct module.body assignments are checked; constants inside functions, classes, or nested blocks are not.
STR006 Module-level constants (SCREAMING_SNAKE_CASE names) must not be assigned mutable list or set literals. Final prevents rebinding but not in-place mutation, so use tuple instead of [...] and frozenset instead of {...} to enforce immutability at the value level. Only plain literals at module scope are checked; dict literals, constructor calls, and comprehensions are not covered.

Suppression

Suppress a single line

x = some_function()  # sergey: noqa
x = some_function()  # sergey: noqa: NAM002
x = some_function()  # sergey: noqa: NAM002, IMP001

Suppress an entire file

Place this comment anywhere in the file (position does not matter):

# sergey: disable-file
# sergey: disable-file: IMP001
# sergey: disable-file: IMP001, IMP002

Development

uv run ruff check .        # lint
uv run ruff format .       # format
uv run ty check            # type check
uv run pytest              # run tests
uv run sergey check .      # run sergey on itself

Adding a rule

  1. Create or extend a module in sergey/rules/ with a class that subclasses base.Rule and implements check(tree, source) -> list[Diagnostic].
  2. Register the rule in sergey/rules/__init__.py by adding an instance to ALL_RULES.
  3. Add tests in tests/rules/.
  4. Update the Rules section in this README to document the new rule.

Rule IDs follow the pattern CAT### where CAT is a short category prefix (IMP, NAM, STR, …) and ### is a three-digit number.

Metadata

Release files for sergey-lint 0.1.3

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

Source distribution (sdist)

Source distribution for sergey-lint 0.1.3
File Size Uploaded
sergey_lint-0.1.3.tar.gz 34.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sergey-lint 0.1.3
File Interpreter ABI Platform
sergey_lint-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 65.4 kB

Release files / sergey_lint-0.1.3.tar.gz

Download URL sergey_lint-0.1.3.tar.gz
Size 34.0 kB
Tags Source
SHA-256 checksum
How to use checksums
aafff3021e5482602854320b72e1a299def4d201c48f9b705d5269d069473523
BLAKE2b-256 checksum
How to use checksums
17011eda0d4e86108974708106cb51700e0b4a335c97a96e043fda0163da573e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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":true}

Release files / sergey_lint-0.1.3-py3-none-any.whl

Download URL sergey_lint-0.1.3-py3-none-any.whl
Size 31.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2e344dcb9d312cf6fdaf3d5fed63b81b1bc5f767b67b901fba8211bde6b1385a
BLAKE2b-256 checksum
How to use checksums
e4d3480e20fed7115fe3fd3c5e4fd49b07acdc9dbfa4455b727ecf12bfc37038
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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":true}

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

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