Skip to content

Commit 1dbbe49

Browse files
committed
docs: add python style guide
1 parent ed93026 commit 1dbbe49

2 files changed

Lines changed: 374 additions & 1 deletion

File tree

docs/definitions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ An [AI Horde](#ai-horde) [Bridge](#bridge) providing [text2img](#text2img), [img
174174

175175
### Scribe
176176

177-
An [AI Horde](#ai-horde) [Bridge](#bridge) providing [LLM](#llm) Generation capabilities. Scribes generate [kudos](#kudos) per text generation with the baseline being 10 kudos for 80 [tokens](tokens) in a 2.7b model. The kudos generated scale with the number of parameters in the model size. Scribes also receive uptime kudos every 10 minutes, with the number increasing with the parameters in their model.
177+
An [AI Horde](#ai-horde) [Bridge](#bridge) providing [LLM](#llm) Generation capabilities. Scribes generate [kudos](#kudos) per text generation with the baseline being 10 kudos for 80 [tokens](#token) in a 2.7b model. The kudos generated scale with the number of parameters in the model size. Scribes also receive uptime kudos every 10 minutes, with the number increasing with the parameters in their model.
178178

179179
### Alchemist
180180

docs/meta/python.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Haidra Python Style Guide
2+
3+
## Too long; didn't read
4+
5+
If this is your first time contributing, reference this document as you work rather than memorizing it. Many guidelines are enforced by linting or testing tools, and other rules can be followed by matching existing codebase patterns.
6+
7+
In brief:
8+
9+
- Descriptive, unambiguous naming is required; avoid abbreviations and acronyms unless widely understood.
10+
- Never silently handle exceptions; always log or re-raise, and avoid blanket or bare excepts.
11+
- All public APIs must have Google-style docstrings.
12+
- Readability first: Prefer guard clauses, clear control flow, meaningfully named boolean expressions and avoid deeply nested structures.
13+
- Type hints are mandatory for all public functions, methods, class attributes, and module-level variables.
14+
- Code should be written for static analysis:
15+
- Avoid magic strings/numbers and direct dictionary access by key literals.
16+
- Prefer classes over dictionaries/tuples for data structures.
17+
- Use `Enum`/`StrEnum` for fixed sets of values.
18+
19+
These principles are opinionated and exist for consistency and maintainability. They don't claim to be the "best" way. Change proposals are welcome if something seems overly restrictive, missing, or could be improved.
20+
21+
## General Principles
22+
23+
- [PEP 20](https://peps.python.org/pep-0020/) should guide design and implementation, but not followed blindly.
24+
- **All** function arguments and return values, class attributes and fields, and module-level variables must be type hinted.
25+
- See the [mypy type hint cheat sheet](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html) for a primer.
26+
- Local variables only need type hints if mypy reports errors, but consider it good practice.
27+
- Use Python 3.10+ union types (`int | str`) instead of `typing.Union[int, str]` and use `| None` for optional types.
28+
- For most version of python, `typing_extensions` is a requirement to enable many used typing related features.
29+
- For self annotation and related functionality, begin the module with `from __future__ import annotations`.
30+
31+
## Naming Conventions
32+
33+
### Module and Package Naming
34+
35+
- Use lower snake_case with underscores between significant words or abbreviations.
36+
- Example: `ai_horde_api`, `generic_api`, `generation_parameters`
37+
- Never use python builtin library names or popular third-party library names.
38+
- Banned examples: `logging`, `json`, `requests`
39+
40+
### Variable, Function, Method and Class Naming
41+
42+
- snake_case for variables, fields, functions, and methods with underscores between significant words or abbreviations.
43+
- CamelCase for classes.
44+
- ALL_CAPS_WITH_UNDERSCORES for constants.
45+
- Prefix private variables and methods with a single underscore (`_private_variable`).
46+
- **Do not reuse variable names** in overlapping scopes (e.g., outer function and inner function, loop variable and surrounding function).
47+
- For example:
48+
49+
```python
50+
# Bad
51+
def process_many_items(some_items: list[Item], special_items: list[SpecialItem]) -> None:
52+
53+
for item in some_items:
54+
process_item(item)
55+
56+
57+
for item in special_items:
58+
# item type is different here, causing confusion, breaks static analysis
59+
process_item(item)
60+
61+
# Good
62+
def process_many_items(some_items: list[Item], special_items: list[SpecialItem]) -> None:
63+
64+
for some_item in some_items:
65+
process_item(some_item)
66+
67+
for special_item in special_items:
68+
process_item(special_item)
69+
70+
```
71+
72+
- **Names must be descriptive**. While ambiguity cannot always be avoided, strive for clarity.
73+
- Avoid vague names like `data`, `info`, `item`, `value`, `name` when a more specific name is possible.
74+
- Context is important. Small functions or loops do not always need extremely descriptive names.
75+
- One/two/three letter variable names only allowed for:
76+
- Very small scope (e.g., `i` for a loop)
77+
- Mathematical context where variables have no significant meaning or are known by convention
78+
- Preserving external module/library naming for consistency
79+
- Avoid abbreviations unless **widely** understood (`url`, `api`, `id`) **and** they significantly improve readability.
80+
- "Widely" means most developers understand without lookup, not "common in some codebase" or "common in a narrow field."
81+
- Acceptable: `id`, `db`, `param`, `anon`, `obj`
82+
- Avoid: `img`, `num`, `cnt`, `val` (not considerably shorter, hurts readability)
83+
- Note: Python builtins methods such as `id` should *not* be used as the entire name of a variable.
84+
- For example, avoid:
85+
86+
```python
87+
# Bad
88+
id = get_user_id()
89+
90+
# Good
91+
user_id = get_user_id()
92+
```
93+
94+
- For cases of remote API compatibility where `id` is required, prefer `id_` or `identifier` and alias appropriately. With pydantic models, use field aliases.
95+
- Acronyms should be widely understood and significantly improve readability.
96+
- Acceptable: `HTTP`, `URL`, `API`, `JSON`, `XML`, `HTML`
97+
- Avoid domain-specific or codebase-specific acronyms:
98+
99+
```python
100+
# Bad
101+
hr = HordeRequest(...)
102+
103+
# Good
104+
horde_request = HordeRequest(...)
105+
```
106+
107+
## Error Handling
108+
109+
- Never silently handle exceptions. Always log and/or re-raise.
110+
- Use exceptions for exceptional cases, not control flow.
111+
- Bare `except:` statements forbidden (they catch system-exiting exceptions like `KeyboardInterrupt`).
112+
- Avoid blanket catches (`except Exception as e:`) unless necessary.
113+
- If used, consider making excepts opt-in with default `raise e`.
114+
- Exception: resource cleanup or finalization tasks (e.g., `__exit__` methods).
115+
- Otherwise, catch only specific expected exceptions you can handle appropriately.
116+
117+
## Documentation and Docstrings
118+
119+
- All public modules, classes, methods, variables and fields must have [Google-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) docstrings.
120+
- First line should be imperative mood and briefly summarize purpose (avoid restating method name unless narrow/trivial).
121+
- `@override` methods inherit parent docstrings unless behavior significantly differs (consider refactoring if so).
122+
123+
- Module docstrings:
124+
- Summarize the module's purpose.
125+
- List and briefly describe critical public classes, functions, and variables.
126+
- Provide any additional context, including caveats about import order or side effects.
127+
- Class docstrings:
128+
- Summarize the class's purpose and behavior.
129+
- Data-class or pydantic models' docstring should begin with "Represents..."
130+
- Add any additional sections as appropriate (e.g., `Examples:`).
131+
- See also the [mkdocs enabled projects](#mkdocs-enabled-projects) section below.
132+
- Function/Method docstrings:
133+
- Methods that:
134+
- Return a value, should start with "Return..."
135+
- Unless the method is a factory/creator method or has a special semantic meaning
136+
- For example:
137+
- A method decorator with `@contextmanager` should start with "Context manager {that/for/etc}..."
138+
- A method that modifies an object in place should start with "Mutate..."
139+
- Async methods should not document the Coroutine object itself, but the eventual returned value.
140+
- It should, however, highlight that it is async if not obvious from the name.
141+
- Create a returned value should start with "Create a {type/description}..."
142+
- Primarily convert, parse, or validate data should start with "Convert...", "Parse...", or "Validate..."
143+
- Other conventions beyond this are flexible, but should remain consistent within a codebase.
144+
- Use the `Args:` section to document each parameter, if any.
145+
- Args should be listed in the same order as the function signature.
146+
- Use the `Returns:` section to document the return value, if any.
147+
- Use the `Raises:` section to document any exceptions that may be raised.
148+
- Document likely exceptions (`FileNotFoundError`, `IOError` for file operations; `ConnectionError`, `TimeoutError` for network calls).
149+
- Note: This does not mean you need to document every possible exception that could be raised, only those that are part of the method's contract, are likely to be encountered by users of the method, or foreseeable by the method's implementation.
150+
151+
### mkdocs enabled projects
152+
153+
- Class docstrings should use the following additional headers as appropriate (these are not required for every class, only when relevant):
154+
- `Thread Safety`: Document whether the class is thread-safe, and any synchronization mechanisms used.
155+
- `Subclass Integration`: Especially for abstract base classes, document how subclasses should implement/extend behavior.
156+
- `Examples`: Provide usage examples, especially for complex classes or those with non-obvious behavior.
157+
- Any additional headings which are appropriate for the specific class. For example, a class representing a network connection might include a `Connection Management` section.
158+
- Function docstrings should use the following additional header if appropriate (these are not required for every function, only when relevant):
159+
- `Side Effects`: Document any side effects, such as modifying global state, files, or network resources.
160+
- `Concurrency`: Document whether the function is thread-safe, and any synchronization mechanisms used.
161+
- `Performance`: Note any performance considerations, such as time complexity or resource usage.
162+
- `Examples`: Provide usage examples, especially for complex functions or those with non-obvious behavior.
163+
- Any additional headings which are appropriate for the specific function. For example, a function that performs network I/O might include a `Network Behavior` section.
164+
- Use mkdocstring links where appropriate to reference other documented classes, methods, or functions within the same codebase.
165+
- You must use the fully qualified name (including module path) for all cross-references.
166+
- For example:
167+
- ```[`Object 1`][full.path.object1] # With a custom title```
168+
- ```[`Object 2`][full.path.object2] # With the identifier as title```
169+
- Only make cross-references to external libraries if they have been configured for this in the mkdocstring configuration.
170+
171+
- See the [mkdocstring documentation](https://mkdocstrings.github.io/usage/#cross-references/) for details on syntax and usage.
172+
173+
## Function and Method Signatures
174+
175+
- Prefer keyword-only arguments when multiple arguments have the same type or are adjacent, improving readability, preventing order mistakes and allowing future signature changes without breaking callers:
176+
177+
```python
178+
# Avoid
179+
def create_user(name: str, age: int, username: str, email: str):
180+
181+
# Prefer
182+
def create_user(*, name: str, age: int, username: str, email: str):
183+
```
184+
185+
## Object-Oriented Design
186+
187+
- Embrace Python's dynamic nature for public interfaces when doing so follows common python idioms and improves usability.
188+
- Strict `isinstance(...)` checks are discouraged unless necessary for network IO/user input safety.
189+
- Use inheritance for "is a" relationships, composition for "has a" relationships.
190+
- Favor interfaces and abstract base classes for public APIs (allows implementation flexibility).
191+
- Use `@override` when overriding methods.
192+
193+
## Method Overloading and Return Types
194+
195+
- Avoid surprisingly overloaded methods with ambiguous behavior. Create separate methods instead:
196+
197+
```python
198+
# Bad
199+
def process_item(item: Item) -> None:
200+
if hasattr(item, '__iter__'):
201+
# handles both single items and lists unpredictably
202+
203+
# Good
204+
def process_items(items: list[Item]) -> None:
205+
def process_single_item(item: Item) -> None:
206+
process_items([item])
207+
```
208+
209+
- Return predictable types:
210+
- Avoid returning `None` as catch-all failure indication. Raise exceptions or return specific failure values.
211+
- `None` should only indicate its usual meaning (missing/unset), not overloaded failure types.
212+
- Accept abstract types (`Iterable`, `Mapping`) for parameters, return concrete types unless specifically designed otherwise:
213+
214+
```python
215+
# Return: concrete so consumers know what to expect
216+
def get_items(self) -> list[Item]: # Not Iterable[Item]
217+
218+
# Parameter and return concrete type: specific expected type
219+
def parse_item(item: Item) -> ParsedItem:
220+
221+
# Parameter and return type: abstract for flexibility, as callers can pass various types
222+
def mutate_items(items: Iterable[Item]) -> Iterable[Item]:
223+
```
224+
225+
> Note: This is not a hard and fast rule; use judgment based on context and the likelihood that the internal implementation or consumer usage may change.
226+
227+
- Use `Any` judiciously; only when more specific types aren't possible and consumers don't need to know the type.
228+
- Methods should not return different types based on parameters:
229+
230+
```python
231+
# Bad
232+
def get_items(self, as_list: bool = True) -> list[Item] | set[Item]:
233+
234+
# Good
235+
def get_items(self) -> list[Item]:
236+
def get_unique_items(self) -> set[Item]:
237+
```
238+
239+
- Always return containers consistently, even for single items:
240+
241+
```python
242+
# Bad
243+
def get_items(self) -> list[Item] | Item:
244+
245+
# Good
246+
def get_items(self) -> list[Item]:
247+
```
248+
249+
## Control Flow and Readability
250+
251+
- Prefer guard clauses over deeply nested if statements:
252+
253+
```python
254+
# Avoid
255+
def process_item(item: Item):
256+
if item is not None:
257+
if item.is_valid():
258+
# process item
259+
260+
# Prefer
261+
def process_item(item: Item):
262+
if item is None or not item.is_valid():
263+
return
264+
# process item
265+
```
266+
267+
- Use meaningfully named composite `bool` conditionals:
268+
269+
```python
270+
# Bad - complex nested conditions that are hard to understand
271+
def do_request(request: Request, worker: Worker) -> bool:
272+
if ((request.model in worker.models and request.size <= worker.max_size) or
273+
request.is_priority) and worker.available and request.safe:
274+
# process request
275+
return True
276+
# skip request
277+
return False
278+
279+
# Better - break down complex logic into named intermediate variables
280+
def do_request(request: Request, worker: Worker) -> bool:
281+
if can_process_request(request, worker):
282+
# process request
283+
return True
284+
# skip request
285+
return False
286+
287+
def can_process_request(request: Request, worker: Worker) -> bool:
288+
model_compatible = request.model in worker.models and request.size <= worker.max_size
289+
can_accept = model_compatible or request.is_priority
290+
291+
return can_accept and worker.available and request.safe
292+
293+
# Even Better - extract validation logic into focused, reusable methods
294+
def do_request(request: Request, worker: Worker) -> bool:
295+
if can_process_request(request, worker):
296+
# process request
297+
return True
298+
# skip request
299+
return False
300+
301+
def can_process_request(request: Request, worker: Worker) -> bool:
302+
return _is_request_compatible(request, worker) and _worker_is_ready(worker, request)
303+
304+
def _is_request_compatible(request: Request, worker: Worker) -> bool:
305+
return (request.model in worker.models and request.size <= worker.max_size) or request.is_priority
306+
307+
def _worker_is_ready(worker: Worker, request: Request) -> bool:
308+
return worker.available and request.safe
309+
```
310+
311+
## Data Structures, Models, and Constants
312+
313+
- Prefer classes over dictionaries or anonymous data structures.
314+
- Use [pydantic](https://docs.pydantic.dev/) `BaseModel` for data structures when validation/conversion is needed.
315+
- Use simple classes when robust validation isn't needed or performance is a concern.
316+
- Use properties for read-only access; avoid exposing mutable members (return copies instead).
317+
- Magic strings/numbers are evil. Use `StrEnum`/`Enum` for specific valid values, constants for isolated values. Members should be capitalized.
318+
- Use `StrEnum` when string representation is needed (e.g., JSON serialization, external APIs):
319+
320+
```python
321+
from enum import auto()
322+
from strenum import StrEnum
323+
324+
class Color(StrEnum):
325+
RED = auto()
326+
GREEN = auto()
327+
BLUE = auto()
328+
```
329+
330+
- Group related constants into classes:
331+
332+
```python
333+
class APIConfig:
334+
MAX_RETRIES = 5
335+
TIMEOUT = 30
336+
JITTER = 0.1
337+
```
338+
339+
- If your settings are user-configurable, use `pydantic-settings`'s `BaseSettings` for environment variable support:
340+
341+
```python
342+
from pydantic import BaseModel
343+
from pydantic_settings import BaseSettings
344+
345+
class MyAppSubModuleSettings(BaseModel):
346+
feature_enabled: bool = True
347+
max_connections: int = 10
348+
349+
class AppSettings(BaseSettings):
350+
api_key: str
351+
debug_mode: bool = False
352+
submodule: MyAppSubModuleSettings = MyAppSubModuleSettings()
353+
354+
class Config:
355+
env_file = ".env"
356+
```
357+
358+
## Imports and Module Export
359+
360+
- Star imports (`import * from <module>`) are forbidden.
361+
- Significant namespaces must explicitly export public members via `__all__`.
362+
363+
## Third-Party Library Specific Guidelines
364+
365+
### Pydantic BaseModel Usage
366+
367+
- Use `BaseModel` derived classes DataClass-like without side effects or state mutation.
368+
- Methods should be limited to validation/conversion with narrow scope - avoid business logic.
369+
- Functions extending beyond `isinstance` or value checking should likely be moved elsewhere.
370+
- In the case you find yourself needing more complex behavior, consider using a regular class which contains a `BaseModel` instance as a member. For smaller scopes, consider utility functions that accept the model as an argument.
371+
- Never coerce non-`None` values to `None` for optional fields.
372+
- API responses and client/server/worker data should be frozen using `model_config`.
373+
- Use any existing default configuration factories in your codebase. In many Haidra-Org modules, this is in the `__init__.py` file of the top-level package.

0 commit comments

Comments
 (0)