Add your description here
Project description
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
- Create or extend a module in
sergey/rules/with a class that subclassesbase.Ruleand implementscheck(tree, source) -> list[Diagnostic]. - Register the rule in
sergey/rules/__init__.pyby adding an instance toALL_RULES. - Add tests in
tests/rules/. - Update the
Rulessection 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sergey_lint-0.1.2.tar.gz.
File metadata
- Download URL: sergey_lint-0.1.2.tar.gz
- Upload date:
- Size: 31.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88455f8ddc9435fe1746f1d5ec68b176d138a9e7d7e764d7ac146b4d4dd40221
|
|
| MD5 |
96c685ed46af154700dba7dfa33563ca
|
|
| BLAKE2b-256 |
83dbb7856b4f2754b2e7882358d6c1d9df1fc5685133896e2a7a92646967818b
|
File details
Details for the file sergey_lint-0.1.2-py3-none-any.whl.
File metadata
- Download URL: sergey_lint-0.1.2-py3-none-any.whl
- Upload date:
- Size: 29.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e78c2392ef850f5604c4ab7aba8e3e07460d95331e881c6d2e9739c2e60b8b0
|
|
| MD5 |
5f142ce3a5c0fe219d8fbed84cb3985b
|
|
| BLAKE2b-256 |
f45a5265c792e57a37890a434abc02464f13bb7dd578b9c8e87869c7097e1710
|