Formatting, Linting, and Documentation
Formatting, Linting, and Documentation
ruff format is not optional. You do not argue about spaces around =. You run the formatter. ruff check catches unused names and a handful of real mistakes. A docstring is a """triple-quoted string""" right under a def, and help() reads it.
Mental model
Three layers, in this order:
- Format — how the file looks (
ruff format). Mechanical. No taste. - Lint — cheap static checks (
ruff check). Unused imports, unused variables, a few footguns. - Docs — a sentence for a function another person (or you, in six months) will call. Not a restatement of the name.
help(fn) prints the function’s signature and docstring. It works on your functions and on the built-ins.
Add the tool once:
uv add --dev ruffWorked examples
Case 1: Format is non-negotiable
Save as messy.py. It runs. It looks like a fight.
# messy.py
def main( ):
tables=8
print( f"tables {tables}" )
if __name__=="__main__":
main()Run:
uv run ruff format messy.py
uv run python messy.pyFormatter output:
1 file reformatted
Program output:
tables 8
The file on disk is now:
# messy.py
def main():
tables = 8
print(f"tables {tables}")
if __name__ == "__main__":
main()Do not hand-match this style. Run ruff format. In a project, uv run ruff format . formats every Python file.
Case 2: A lint finding, then the fix
Save as lint_bug.py. The unused import is the bug ruff check is for.
# lint_bug.py
import json
def main():
print("shift open")
if __name__ == "__main__":
main()Run:
uv run python lint_bug.py
uv run ruff check lint_bug.pyThe program still prints:
shift open
The linter reports:
F401 [*] `json` imported but unused
--> lint_bug.py:2:8
|
1 | # lint_bug.py
2 | import json
| ^^^^
help: Remove unused import: `json`
Found 1 error.
[*] 1 fixable with the `--fix` option.
Fix — save this as lint_bug.py (or run uv run ruff check --fix lint_bug.py):
# lint_bug.py
def main():
print("shift open")
if __name__ == "__main__":
main()uv run ruff check lint_bug.py
uv run python lint_bug.pyLinter:
All checks passed!
Program:
shift open
F401 is not a taste note. An unused import is a lie about what the file needs. Delete it.
Case 3: A docstring help() can read
Save as ticket_help.py. The first statement in the function is a string. That string is the docstring.
# ticket_help.py
def label(ticket_id, table):
"""Return a one-line label for a ticket at a table."""
return f"ticket {ticket_id} → table {table}"
def main():
help(label)
print(label(7, 12))
if __name__ == "__main__":
main()Run:
uv run python ticket_help.pyOutput:
Help on function label in module __main__:
label(ticket_id, table)
Return a one-line label for a ticket at a table.
ticket 7 → table 12
help(label) reads label.__doc__. You did not write a website. You wrote the sentence that shows up when someone asks the runtime.
Case 4: help() on a built-in
Save as help_len.py.
# help_len.py
def main():
help(len)
if __name__ == "__main__":
main()Run:
uv run python help_len.pyOutput (first lines):
Help on built-in function len in module builtins:
len(obj, /)
Return the number of items in a container.
When you forget an argument, help is faster than a search tab.
The trap
Turning the formatter off because you “like tabs,” or sprinkling # noqa until ruff check is quiet. The team then has two styles and no signal.
The docstring trap is the opposite of silence: a paragraph that repeats the name.
# obvious.py
def add(a, b):
"""Add a and b and return the sum."""
return a + b
def main():
print(add(2, 6))
if __name__ == "__main__":
main()Run:
uv run python obvious.pyOutput:
8
It works. The docstring adds nothing. Write a docstring when the function hides a rule (prices cannot be negative, status is open|paid). Skip it when the name and the signature are the documentation.
The boring rule
- Run
uv run ruff format .before you commit. No debate. - Run
uv run ruff check .. FixF401and friends. Do not blanket-ignore. - Docstrings use
"""triple quotes"""on the line afterdef. - One sentence is enough for most functions.
help(fn)should be readable. - Do not document
add. Do documentline_totalif it rejects negative prices.
Try this
- Break spacing in
ticket_help.pyon purpose. Runuv run ruff format ticket_help.pyand confirm it snaps back. - In
lint_bug.py, addimport sysand do not use it. Confirmruff checkreportsF401again, then delete the import. - Give
labela second sentence in the docstring that says the arrow is literal text. Runticket_help.pyand readhelp()again.