↓ Skip to main content

Software Development Standards

·1013 words·5 mins
Alexander Sokolov aka s0k0l
Author
Alexander Sokolov aka s0k0l
I emulate user behavior, extract data and protect information. I take projects from idea to release.

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.