Files
site/content/code-style.en.md
T
protokeyandClaude Opus 5.5 a4d92fecd3 feat: English version of the site, CV as text
- add English language: config, menus, i18n, translations of all pages and posts
- CV page renders the full resume in both languages with PDF download buttons
- remove salary from CV PDFs, Telegram is @xs0k0lx everywhere, English level B2
- fix broken CV menu link, update theme submodule to Public/blowfish

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:54:42 +04:00

116 lines
6.0 KiB
Markdown

---
title: "Software Development Standards"
date: 2026-10-08T19:18:00+04:00
---
These are the rules I write code by. They are based on Robert Martin's "Clean Code" and PEP 8 for Python. It's a living document, I extend it as I learn things the hard way.
The main idea is simple: code is read far more often than it is written. So I write it for the person who opens the file six months from now. Often that person is me.
## 0. No spec, no result
Any work starts with requirements, not with code.
- First I pin down what problem we're solving and how we'll know it's solved.
- Then the architecture: components, data, the boundaries between them.
- Only after that, code. If requirements change along the way, I update the spec first, then the code.
## 1. Code style: PEP 8 + my rules
PEP 8 is the baseline standard. My rules work on top of it, like the cascade in CSS: everything I haven't overridden is inherited from PEP 8 as is. There's one override.
**Tabs instead of spaces.** One level of nesting is one character. Everyone sees indentation at the width they set in their editor, and nothing changes in the file. I never mix spaces and tabs in one file, Python 3 won't allow it anyway.
Everything else follows PEP 8: line length, blank lines between functions and classes, import order, spaces around operators.
So I don't have to keep this in my head, a tool checks and fixes the style. I use ruff:
```toml
# pyproject.toml
[tool.ruff]
line-length = 100
[tool.ruff.format]
indent-style = "tab"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "B", "UP"]
ignore = ["W191"] # W191 complains about tabs, for us it's a deliberate choice
```
IDE: JetBrains (PyCharm). The project settings have tabs enabled and ruff runs on save.
## 2. Names
A name should answer three questions: why it exists, what it does and how it's used. If a name needs a comment, it's a bad name.
- Variables and functions in `snake_case`, classes in `PascalCase`, constants in `UPPER_CASE`.
- Functions are named with a verb: `fetch_page`, `parse_price`, `send_report`. Classes and variables with a noun: `Browser`, `proxy_pool`.
- No abbreviations or single-letter names. Exceptions: `i` in a short loop and `e` for an exception.
- One concept, one word. If the project has `fetch`, then `get`, `load` and `retrieve` don't show up next to it for the same thing.
- No type encoding in names: `users`, not `users_list` or `lst_users`.
- Boolean variables read like a question: `is_ready`, `has_proxy`, `can_retry`.
## 3. Functions
- **Small.** A function should fit on the screen without scrolling. If it doesn't, it can be split.
- **Do one thing.** If a function can't be described in one sentence without the word "and", it's two functions.
- **One level of abstraction.** A function either orchestrates a process and calls other functions, or works with details. Not both at once.
- **Few arguments.** Ideally zero to two. Three or more is a reason to group them into a dataclass.
- **No flag arguments.** `render(page, True)` is hard to read. Better two functions: `render_full` and `render_preview`.
- **No hidden side effects.** If a function is called `check_session`, it shouldn't create a new session along the way.
- **Command or query.** A function either changes state or returns data. Not both.
## 4. Comments
The best comment is the one that wasn't needed because the code is clear as it is.
I write a comment when I need to explain **why**, not **what**:
```python
# Cloudflare returns 403 on the first request without a cookie, so we always make two
response = session.get(url)
```
I don't write:
- comments that retell the code
- commented-out code. That's what git history is for
- a change log and authorship in the file header. That's git's job too
A docstring is required for a module's public functions and classes. For internal ones, it depends.
## 5. Error handling
- Exceptions instead of return codes. A function doesn't return `None` or `-1` when something goes wrong, it raises an exception.
- I catch specific exceptions, never a bare `except:` or `except Exception: pass`.
- My own exceptions for my domain: `ProxyBannedError`, `CaptchaError`. They tell you what happened without reading the stack trace.
- I don't return `None` where a collection is expected. An empty list is better than a `None` check at every call site.
- I handle an error where I know what to do with it. If I don't know, I let it go up.
## 6. Classes and modules
- **Small classes with a single responsibility.** A class should have one reason to change. If a class talks to the network, parses HTML and writes to the database, that's three classes.
- **High cohesion.** A class's methods work with its fields. If a method doesn't touch `self`, it most likely doesn't belong in this class.
- **Dependencies from outside.** A class receives the browser, session or database client in its constructor instead of creating them itself. That makes it easy to test and swap.
- **Law of Demeter.** I talk only to immediate neighbors. `order.customer.address.city` is a sign that a method is needed.
- **Boundaries with third-party code.** I wrap third-party libraries in my own thin layer. If the library has to be replaced, the change happens in one place.
## 7. Tests
- Tests are code too, and the same cleanliness requirements apply.
- A test checks one thing, and its name shows it: `test_returns_empty_list_when_page_has_no_items`.
- Every test has the same structure: arrange, act, assert.
- Tests are fast, independent of each other and give the same result on every run. No trips to the real internet in unit tests.
- I reproduce a bug with a test first, then fix it.
## 8. The Boy Scout rule
I leave code cleaner than I found it. There's no need to rewrite everything at once. It's enough to rename one unclear variable or extract one piece into a function every time I touch a file.
## 9. Git
- One commit, one logical change.
- Commit messages in the conventional commits format: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`.
- Code that doesn't pass the linter and tests doesn't get into the main branch.
- Secrets, keys and `.env` never get into the repository.