Opinions · My Python Style
My Python Style
How I organize and write research code, so that a collaborator can read it, rerun it and extend it without asking me.
Python Philosophy is about how to think about code; this page is about how my own code looks. It describes habits, not rules: take what helps, and in someone else's repository, follow the style it already has. The examples come from repositories I wrote or helped write: Effortless (fast linear image reconstruction for Roman), PyIMCOM (its predecessor, whose refactoring I led), mesa_apokasc (stellar models of red giants) and ContEvol (a numerical method for differential equations).
How a project is laid out
- A package and its drivers. Reusable logic goes into an importable package named for the method; choices that belong to one run, such as paths and options, go into short driver scripts, each opening with a line that says how to run it.
- Modules in layers. Compute kernels at the bottom, physical models above them, input, output and analysis on top. A lower module never imports a higher one.
- Classes that mirror the data. In Effortless, an output image holds its input images and the small patches it processes one at a time. Each piece reaches its settings through its parent, so a single configuration is passed down rather than copied.
- Settings are data; data stays out of git. PyIMCOM keeps each run's settings in a JSON file and writes the full configuration into its output, defaults included, since defaults change.
Code a reader can follow
- Docstrings everywhere. Each module opens with a list of what it contains; each class and public function has a NumPy-style docstring, with the shape, type and unit of every array.
- Every tunable number has a name. It is written in capitals, with its unit and its source in a comment; derived numbers are computed from it, not typed again. A driver then changes behavior in one visible line.
- Names follow the paper.
psf_in,psf_outandsigmamean what they mean in the equations, and a loop index says what it counts. - Comments explain why. A short comment heads each block of code, and the rest explain choices: "In principle we should flip the input PSF, but in practice it is easier to flip the weight field."
- Errors name what failed, such as the setting that has an invalid value.
Right first, then fast
- A case with a known answer. ContEvol runs every integrator on uniform circular motion at smaller and smaller steps and prints the error, the first test of any new method.
- A regression run for every refactor. Effortless reruns a reference case and checks that the output has not changed.
- Then speed. Hot loops move into compiled functions (Numba), one commit per speedup, with the results checked again after each; large arrays are freed as soon as a stage ends.
- Every output traceable. Random seeds are fixed, the configuration is written into the output, and long batch jobs skip what is already done, so a resubmitted job finishes only what is missing.
Figures from code
- Every figure in a paper is made by a script from saved data and saved as a vector PDF. None is edited by hand, so a referee's comment can be answered by changing the script.
- One shared style (fonts, ticks, and a small helper applied to every panel) keeps figures alike from paper to paper.
- One variable per visual channel: color for the method, line style for a second choice, and markers so that curves survive printing in gray. Every axis has a label with units.
- When the question is how much a choice matters, I plot the ratio to a baseline, with a line at 1.
- Each figure script names the figure it makes in the paper.
Commits that explain
- One logical change per commit, with a short subject that starts with what kind of change it is: "Fix calculation of distortion matrices", "Accelerate
SubSlice.__call__with Numba". - A fix that changes scientific results starts with "IMPORTANT:", so that anyone holding old outputs knows to look.
- A change to how the code is used or configured gets a body that says what changed and why.
- In a shared repository, new features go through a branch and a pull request.
With an AI assistant
An assistant may write the code, but a person must be able to read, check and explain every line of it (Python Philosophy).
- Generated code follows the repository's style, not the assistant's.
- Changes stay small, so each one can be reviewed; a line I cannot explain is simplified or rewritten before it is committed.
- Each commit message says what changed in behavior and why, and credits the assistant.
- The repository keeps a file of instructions for the assistant: how to run and test the code, where the data lives, and what it must never do without asking. MapHub, which I built with AI assistance (Claude), shows these practices together.
Habits I keep
A few of my habits depart from the most common Python style: short statements share a line when they read as one thought (if not mask_out[i]: continue); a class runs its stages through __call__; and flags in exploratory notebooks carry the date they were set. When I contribute to someone else's code, I follow its style, and I never mix a change of style into a commit that changes behavior.
Some habits suit a prototype better than a shared code. As a code gains users, it gains what they need: a package they can install, tests that run on every change, and log messages instead of printed ones.