Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Common Mistakes in Python
|
||||
|
||||
This file captures recurring non-idiomatic patterns, especially code that reads like Java or TypeScript in a Python costume.
|
||||
|
||||
## 1. Boolean-flag methods that hide two behaviors
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
def fetch_user(user_id: str, include_deleted: bool = False) -> User | None:
|
||||
...
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
Boolean mode flags often mean one function is doing two jobs. They create call sites like `fetch_user(id, True)` that say almost nothing.
|
||||
|
||||
### Better shape
|
||||
|
||||
- split into separate functions when the behaviors are meaningfully different
|
||||
- or promote the difference to a real enum or config object when there are several modes
|
||||
|
||||
## 2. Broad `except Exception:` without a boundary reason
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
try:
|
||||
do_work()
|
||||
except Exception:
|
||||
return None
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
This erases recovery meaning. Mature libraries like HTTPX and Click use exception hierarchies so callers can catch broadly or narrowly depending on what they can recover from.
|
||||
|
||||
### Better shape
|
||||
|
||||
- catch the specific failures you can handle
|
||||
- use broad catch points only at real process, API, or CLI boundaries
|
||||
- translate internal failures into a clear outer error contract
|
||||
|
||||
### Source signals
|
||||
|
||||
- `httpx/_exceptions.py:74-90` defines `HTTPError` as a broad catch point.
|
||||
- `httpx/_exceptions.py:107-160` then narrows request, transport, and timeout failures for recovery.
|
||||
- `src/click/exceptions.py:35-111` ties exception subtypes to different exit codes and help output.
|
||||
|
||||
## 3. Hidden I/O in constructors or properties
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
class User:
|
||||
def __init__(self, user_id: str):
|
||||
self.profile = requests.get(...).json()
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
Object creation now performs surprising network I/O, which makes testing, lifetime, and failure handling muddy.
|
||||
|
||||
### Better shape
|
||||
|
||||
- keep I/O in explicit factory or boundary methods
|
||||
- make resource acquisition visible
|
||||
- separate data containers from loading logic
|
||||
|
||||
## 4. Import-time side effects
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
client = connect_to_db()
|
||||
load_config_from_network()
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
Importing a module should usually define names, not secretly talk to the outside world. Import-time side effects make startup order brittle and tests unpredictable.
|
||||
|
||||
### Better shape
|
||||
|
||||
- move initialization into explicit startup paths
|
||||
- create resources in app setup, dependency wiring, or main entrypoints
|
||||
|
||||
## 5. Mixing validation, transport, and business logic in one class
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
class OrderModel(BaseModel):
|
||||
def save(self): ...
|
||||
def send_webhook(self): ...
|
||||
def render_html(self): ...
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
Boundary schema, persistence logic, and business behavior cannot evolve independently anymore.
|
||||
|
||||
### Better shape
|
||||
|
||||
- let Pydantic, attrs, or dataclasses own data-shape concerns
|
||||
- keep boundary translation explicit
|
||||
- split long-lived behavior into services or domain objects when needed
|
||||
|
||||
### Source signals
|
||||
|
||||
- `pydantic/main.py:253-264` treats model construction as input validation.
|
||||
- `pydantic/main.py:455-519` makes dumping an explicit serialization step.
|
||||
- `src/_pytest/timing.py:24-64` uses frozen dataclasses for small internal value objects.
|
||||
|
||||
## 6. Fake sync wrappers around async code
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
def get(url: str) -> Response:
|
||||
return asyncio.run(async_get(url))
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
This breaks in existing event loops and hides real async lifetime and cancellation semantics.
|
||||
|
||||
### Better shape
|
||||
|
||||
- provide separate sync and async entrypoints when semantics differ
|
||||
- keep them parallel, not secretly interchangeable
|
||||
|
||||
### Source signals
|
||||
|
||||
- `httpx/_client.py:594-661` and `httpx/_client.py:1307-1375` define separate `Client` and `AsyncClient` types.
|
||||
- `httpx/_client.py:639-660` vs. `httpx/_client.py:1353-1374` keep the constructor shapes parallel while changing transport types.
|
||||
- `examples/asyncio/async_orm.py:61-67` vs. `examples/inheritance/joined.py:93-120` show the same split in SQLAlchemy session usage.
|
||||
|
||||
## 7. Overuse of `Any`
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
def send(data: Any, options: Any) -> Any:
|
||||
...
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
The API contract disappeared.
|
||||
|
||||
### Better shape
|
||||
|
||||
- use concrete types when the contract is specific
|
||||
- use `Protocol` when callers care about capability
|
||||
- use explicit unions or aliases for ergonomic flexibility
|
||||
|
||||
### Source signals
|
||||
|
||||
- `Lib/typing.py:2132-2157` frames `Protocol` around structural subtyping.
|
||||
- `Lib/typing.py:2155-2157` warns that runtime protocol checks ignore signatures.
|
||||
- `httpx/_client.py:639-660` shows rich public typing instead of `Any`-shaped parameters.
|
||||
|
||||
## 8. Accidental public APIs through file layout
|
||||
|
||||
### Smell
|
||||
|
||||
```python
|
||||
from mypkg.utils import helper_a
|
||||
from mypkg.impl_v2 import thing
|
||||
```
|
||||
|
||||
### Why it is a smell
|
||||
|
||||
Internal module layout becomes the public contract by accident, which makes refactors painful.
|
||||
|
||||
### Better shape
|
||||
|
||||
- publish a deliberate import surface
|
||||
- re-export supported names
|
||||
- use `__all__` when the boundary should be explicit
|
||||
|
||||
### Source signals
|
||||
|
||||
- `src/pytest/__init__.py:6-80` builds a top-level facade over `_pytest.*` internals.
|
||||
- `src/pytest/__init__.py:98-186` pins that facade with `__all__`.
|
||||
- `httpx/__init__.py:1-12` and `httpx/__init__.py:29-106` do the same while also rewriting exported objects to appear under `httpx`.
|
||||
|
||||
## Heuristic
|
||||
|
||||
If the code:
|
||||
|
||||
- hides resource lifetime
|
||||
- hides write boundaries
|
||||
- hides which failures matter
|
||||
- hides what the public API really is
|
||||
|
||||
…it is usually fighting Python instead of using it.
|
||||
Reference in New Issue
Block a user