SLD3xx - Object-Oriented Design

SLD301: Prohibits __init__ and __post_init__ methods.

Other dunder methods (__str__, __repr__, __eq__, __hash__, __call__, etc.) are allowed.

Use @dataclass with default_factory for attributes, or @classmethod for parameter computation:

# Bad
class Processor:
    def __init__(self, client, config):
        self.client = client
        self.config = config

# Good
from dataclasses import dataclass

@dataclass(frozen=True, slots=True, kw_only=True)
class Processor:
    client: HTTPClient
    config: Config

SLD302: Prohibits private methods (starting with _).

Extract private methods into separate classes:

# Bad
class Processor:
    def _validate(self, data):
        ...

# Good
class Validator:
    def validate(self, data):
        ...

SLD303: Flags methods that only access public members of self.

Convert to module-level functions or use functools.singledispatch:

# Bad
class User:
    name: str

    def display_name(self) -> str:
        return self.name.title()

# Good
def display_name(user: User) -> str:
    return user.name.title()

Methods decorated with @override (from typing or typing_extensions) are exempt: the decorator is the author’s declaration that the function lives on the class on purpose, because it fulfills a parent class’s or Protocol’s contract — not because it happens to touch private state.

from typing import override

class JSONRenderer:
    @override
    def render(self, value: object) -> str:
        return json.dumps(value)

SLD304: Flags an expression compared (via ==, !=, or in against a tuple/list/set literal) with two or more distinct string literals within a single function or module scope. Single x == "foo" comparisons are fine (parsers commonly need them); the smell is having multiple candidate values.

# Bad
def handle(status: str) -> int:
    if status == "open":
        return 1
    if status == "closed":
        return 2
    return 0

# Good
from enum import Enum

class Status(Enum):
    OPEN = "open"
    CLOSED = "closed"

def handle(status: Status) -> int:
    if status is Status.OPEN:
        return 1
    if status is Status.CLOSED:
        return 2
    return 0

SLD305: Flags match statements with two or more string-literal case patterns (including case "a" | "b": alternatives). Pattern matching on string values is a strong enum smell.

# Bad
def handle(status):
    match status:
        case "open":
            return 1
        case "closed":
            return 2

SLD306: Flags any string literal that appears in equality contexts (==, !=, in collection, or a match case) three or more times across the module. A value special enough to be checked from many sites should be a named enum member.

SLD307: Flags Literal[...] annotations whose arguments include string literals. Literal["a", "b"] is a lightweight alternative to an enum, but this project prefers a real Enum for the readability and refactoring wins.

# Bad
from typing import Literal

def handle(status: Literal["open", "closed"]) -> int: ...

# Good
from enum import Enum

class Status(Enum):
    OPEN = "open"
    CLOSED = "closed"

def handle(status: Status) -> int: ...

SLD308: Flags two or more peer module-level string constants whose values are valid Python identifiers (e.g. READ = "read"). A cluster of such constants is almost always an enum waiting to be written; defining them loosely lets the rest of the SLD30x checks miss them (the literal never appears in a comparison or match — only the name does). The check ignores values that aren’t str.isidentifier()-true, so things like HOST = "example.com" or GREETING = "hello world" don’t fire.

# Bad
READ = "read"
WRITE = "write"
DELETE = "delete"

# Good
from enum import Enum, auto

class Action(Enum):
    READ = auto()
    WRITE = auto()
    DELETE = auto()

SLD309: Flags Enum subclasses (including StrEnum, IntEnum, Flag, IntFlag) where every string-constant member has an identifier-shaped value. Such enums leak a stringly-typed backdoor: MyEnum("read") round-trips a bare string into a member. Use auto() instead — it generates unique values without exposing identifier-shaped strings. Non-identifier values ("#ff0000", "application/json") are kept; the check only fires when every string member is identifier-shaped.

# Bad
from enum import Enum

class Action(Enum):
    READ = "read"
    WRITE = "write"

# Good
from enum import Enum, auto

class Action(Enum):
    READ = auto()
    WRITE = auto()