Review existing Python code and write new Python code following the 90 best practices from "Effective Python" by Brett Slatkin (2nd Edition). Use when writing Python, reviewing Python code, or wanting idiomatic, Pythonic solutions.
Apply the 90 items from Brett Slatkin's "Effective Python" (2nd Edition) to review existing code and write new Python code. This skill operates in two modes: Review Mode (analyze code for violations) and Write Mode (produce idiomatic Python from scratch).
This skill includes categorized reference files with all 90 items:
ref-01-pythonic-thinking.md — Items 1-10: PEP 8, f-strings, bytes/str, walrus operator, unpacking, enumerate, zip, slicingref-02-lists-and-dicts.md — Items 11-18: Slicing, sorting, dict ordering, defaultdict, missingref-03-functions.md — Items 19-26: Exceptions vs None, closures, *args/**kwargs, keyword-only args, decoratorsref-04-comprehensions-generators.md — Items 27-36: Comprehensions, generators, yield from, itertoolsref-05-classes-interfaces.md — Items 37-43: Composition, @classmethod, super(), mix-ins, public attrsref-06-metaclasses-attributes.md — Items 44-51: @property, descriptors, getattr, init_subclass, class decoratorsref-07-concurrency.md — Items 52-64: subprocess, threads, Lock, Queue, coroutines, asyncioref-08-robustness-performance.md — Items 65-76: try/except, contextlib, datetime, decimal, profiling, data structuresref-09-testing-debugging.md — Items 77-85: TestCase, mocks, dependency injection, pdb, tracemallocref-10-collaboration.md — Items 86-90: Docstrings, packages, root exceptions, virtual environmentsBefore responding, read the relevant reference files based on the code's topic. For a general review, read all files. For targeted work (e.g., writing async code), read the specific reference (e.g., ref-07-concurrency.md).
When the user asks you to review existing Python code, follow this process:
Determine which chapters apply to the code under review and read those reference files. If unsure, read all of them.
If the code is already well-written and idiomatic:
If the code has real problems:
When the code uses these patterns correctly, explicitly praise them:
<strengths_to_praise>
@contextmanager for resource management: "Good use of @contextmanager (Item 66) — avoids boilerplate try/finally and makes the cleanup intent clear."yield) for memory efficiency: "Good use of a generator (Item 30) — avoids loading the entire sequence into memory."@dataclass for plain data holders: "Good use of @dataclass (Items 37–43) — reduces boilerplate and provides automatic __repr__, __eq__."enumerate instead of range(len(...)): "Good use of enumerate (Item 7)."
</strengths_to_praise>For each relevant item from the book, check whether the code follows or violates the guideline. Focus on:
<core_principles>
@dataclass used for plain data holders?<anti_patterns>
Mutable default arguments (Item 24): def f(items=[]) is a critical bug — the list is shared across all calls. Always use None and initialize inside the function body.
# WRONG — shared mutable default
def process(results=[]):
results.append(...)
# RIGHT — use None sentinel
def process(results=None):
if results is None:
results = []
results.append(...)
Bare except: clause (Item 65): except: without a type catches KeyboardInterrupt, SystemExit, and GeneratorExit, silently killing the program. Always catch specific exception types: except (ValueError, KeyError): or at minimum except Exception:.
for i in range(len(seq)) (Item 7): Use for item in seq directly, or for i, item in enumerate(seq) when you need the index.
Manual list-building loops (Item 27): Any loop that creates an empty list and appends inside the loop body should be a list comprehension.
# WRONG
result = []
for x in items:
if x > 0:
result.append(x * 2)
# RIGHT
result = [x * 2 for x in items if x > 0]
Java-style getter/setter methods (Item 44): get_name(), set_price(), get_value() are non-Pythonic. Access attributes directly or use @property when validation is required.
== True / == False comparisons (Item 2 / PEP 8): if x == True: should be if x:. return self.in_stock == True should be return self.in_stock.
Double-underscore name mangling (Item 42): self.__items makes the attribute inaccessible to subclasses and creates maintenance friction. Use single underscore self._items to signal "internal use" without enforced hiding.
Plain data-holder class without @dataclass (Items 37–43): Any class whose __init__ only assigns parameters to self.attr with no logic should be a @dataclass. Dataclasses automatically generate __repr__, __eq__, and __init__, and signal the data-holder intent. Crucially: @dataclass and @property can coexist. If one field needs validation, make it a @property with a setter inside the @dataclass. This is the correct Pythonic pattern — do NOT abandon @dataclass just because one field has a validator.
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
category: str
in_stock: bool = True
_price: float = field(default=0.0, repr=False)
@property
def price(self) -> float:
return self._price
@price.setter
def price(self, value: float) -> None:
if value < 0:
raise ValueError('Price cannot be negative')
self._price = value
Missing __repr__ (Items 37–43): Any class that is not a @dataclass should define __repr__ to aid debugging. Without it, repr(obj) shows only the class name and memory address.
Returning None for failure (Item 20): Functions should raise exceptions for error conditions, not return None. Returning None forces callers to check for None every time and doesn't carry error information.
else block after for/while (Item 9): The loop-else clause fires when the loop completes without a break, which is rarely the intended semantics and confuses readers. Avoid it.
</anti_patterns>
</core_principles>
For each issue found, report:
Offer a corrected version of the code with all issues addressed, with comments explaining each change.
When the user asks you to write new Python code, follow these principles:
Use f-strings for string formatting (Item 4). Never use % or .format() for simple cases.
Use unpacking instead of indexing (Item 6). Prefer first, second = my_list over my_list[0].
Use enumerate instead of range(len(...)) (Item 7).
Use zip to iterate over multiple lists in parallel (Item 8). Use zip_longest from itertools when lengths differ.
Avoid else blocks after for/while loops (Item 9).
Use assignment expressions (:= walrus operator) to reduce repetition when appropriate (Item 10).
Raise exceptions instead of returning None for failure cases (Item 20).
Use None as the default for mutable default arguments (Item 24). Never use [], {}, or any other mutable object as a default argument value; initialize inside the function body.
Use keyword-only arguments for clarity (Item 25). Use positional-only args to separate API from implementation (Item 25).
Use functools.wraps on all decorators (Item 26).
Prefer comprehensions over map/filter (Item 27). Keep them simple — no more than two expressions (Item 28).
Use generators for large sequences instead of returning lists (Item 30).
Use @dataclass for plain data-holder classes (Items 37–43). A @dataclass automatically provides __init__, __repr__, and __eq__, and makes the data-holder intent explicit. Only write a manual __init__ when you need real logic that a dataclass can't handle. Add __repr__ to any class that doesn't use @dataclass, to make debugging easier.
Prefer composition over deeply nested classes (Item 37).
Use @classmethod for polymorphic constructors (Item 39).
Always call super().init (Item 40).
Use plain attributes instead of getter/setter methods. Use @property for special behavior (Item 44).
Use try/except/else/finally structure correctly (Item 65). Always catch specific exception types, never bare except:.
Write docstrings for every module, class, and function (Item 84).
</guidelines>When writing new modules or classes, follow this structure:
"""Module docstring describing purpose."""
# Standard library imports
# Third-party imports
# Local imports
# Module-level constants
class MyClass:
"""Class docstring describing purpose and usage.
Attributes:
attr_name: Description of attribute.
"""
def __init__(self, param: type) -> None:
"""Initialize with description of params."""
self.param = param # Use public attributes (Item 42)
@classmethod
def from_alternative(cls, data):
"""Alternative constructor (Item 39)."""
return cls(processed_data)
def method(self, arg: type) -> return_type:
"""Method docstring.
Args:
arg: Description.
Returns:
Description of return value.
Raises:
ValueError: When arg is invalid (Item 20).
"""
pass
</example>
<example id="2" title="Mutable default argument — correct pattern">
# WRONG — mutable default causes shared state across all calls
def append_to(element, to=[]):
to.append(element)
return to
# RIGHT — use None sentinel, initialize inside
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
</example>
<example id="3" title="Plain data holder — use @dataclass, even with @property validation">
# WRONG — manual __init__ boilerplate for data holder
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# RIGHT — @dataclass provides __init__, __repr__, __eq__ for free
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
Important: @dataclass and @property are compatible. When one field needs validation, use both — the @dataclass handles the boilerplate and the @property handles validation. Do NOT fall back to a plain class just because one field has a setter.
# WRONG — abandoning @dataclass because price needs validation
class Product:
def __init__(self, name, price, category):
self.name = name
self.price = price # validation via set_price()
self.category = category
def set_price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self.price = value
# RIGHT — @dataclass + @property work together
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
category: str
in_stock: bool = True
_price: float = field(default=0.0, repr=False)
@property
def price(self) -> float:
return self._price
@price.setter
def price(self, value: float) -> None:
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
</example>
<example id="4" title="Getter/setter vs plain attribute and @property">
# WRONG — Java-style getter/setter
class Temperature:
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
# RIGHT — use @property for validation, direct access otherwise
class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius # triggers setter on construction
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
</example>
<example id="5" title="List comprehension vs manual loop">
# WRONG — manual loop to build list
result = []
for order in orders:
if order['total'] > threshold:
result.append(order)
# RIGHT — list comprehension
result = [order for order in orders if order['total'] > threshold]
</example>
</examples>
subprocess for managing child processes (Item 52)threading.Lock to prevent data races (Item 54)Queue for coordinating work between threads (Item 55)asyncio for highly concurrent I/O (Item 60)TestCase and use setUp/tearDown (Item 78)unittest.mock for complex dependencies (Item 78)pdb.set_trace() or breakpoint() for debugging (Item 80)tracemalloc for memory debugging (Item 81)When time is limited, focus on these highest-impact items first:
[] or {})for item in seq or enumerate; never range(len(seq))@dataclass for plain data holders; add __repr__ to any class without it@contextmanager for reusable resource management patternsWhen the submitted code is already idiomatic and well-structured, the review must:
@contextmanager usage → praise as Item 66yield) → praise as Item 30@dataclass for data holders → praise as Items 37–43Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer