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

6.0 KiB

title, date
title date
Software Development Standards 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:

# 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:

# 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.