The most useful Python command is often the one that answers a narrow question: Which interpreter is this? What was imported? Are the dependencies coherent? Where is the time going? I use the following commands as a ladder. Start with the cheapest check and move downward only when the evidence requires it.
Examples use python; on systems where Python 3 is exposed as python3, substitute that executable consistently. On Windows, the Python launcher commonly uses py.
Prove which Python is running
Before installing or debugging anything, establish the interpreter and environment:
python --version
python -VV
python -c "import sys; print(sys.executable)"
python -c "import sys; print(sys.prefix); print(sys.base_prefix)"
python -m site
--versionis the quick check;-VValso reports build details.sys.executableis the interpreter that will execute-m pip, tests, and modules.- In a virtual environment,
sys.prefix != sys.base_prefixis normally true. python -m siteprints the active import paths and user-site state.
For a concise, copyable run record:
python -c "import platform, sys; print(sys.executable); print(platform.platform()); print(sys.version)"
Do not infer the active interpreter from the shell prompt alone. An environment name can be stale; sys.executable is direct evidence.
Choose the execution mode deliberately
Python has several entry modes, each useful for a different job:
| Need | Command | Why |
|---|---|---|
| Run a file | python script.py --input data.csv |
Simple scripts and explicit arguments |
| Run an installed module | python -m package.module |
Uses the selected interpreter’s import system |
| Run a short probe | python -c "import pandas; print(pandas.__version__)" |
Fast, non-interactive evidence |
| Inspect state after a script | python -i script.py |
Drops into the REPL after execution |
| Enable extra runtime checks | python -X dev script.py |
Surfaces warnings and costly development checks |
| Trace slow imports | python -X importtime -c "import pandas" |
Separates import cost from analysis cost |
| Run isolated from user settings | python -I script.py |
Excludes the current directory, user site, and PYTHON* variables |
Prefer python -m tool over a bare tool command when interpreter identity matters:
python -m pip --version
python -m pytest -q
python -m myproject.validate --manifest samples.csv
The -m form locates the module with that Python’s standard import mechanism. This prevents a common failure in which pip, pytest, and python resolve to different environments.
Create and audit an environment
For a small pure-Python project, the standard library is sufficient:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
An editable install is useful during development because imports resolve to the working tree. CI and release checks should also test a regular install, because editable and built installs can behave differently.
Use these commands to answer different environment questions:
python -m pip list
python -m pip list --outdated
python -m pip show pandas
python -m pip check
python -m pip inspect > environment.json
python -m pip freeze > requirements.snapshot.txt
| Command | Answers | Does not prove |
|---|---|---|
pip show NAME |
Version, location, metadata, declared requirements | That the whole environment is coherent |
pip check |
Whether installed packages have compatible declared dependencies | Scientific validity or undeclared system libraries |
pip inspect |
Machine-readable installed-package and environment metadata | That another machine will resolve the same artifacts |
pip freeze |
What is installed now, in requirements syntax | A solver result or true lockfile |
Treat pip freeze as a snapshot for diagnosis, not as the sole project specification. Keep direct dependencies in pyproject.toml; for long-lived or regulated work, also use an appropriate lock or hashed requirements workflow and retain the Python, OS, container, and pipeline metadata.
When finished:
deactivate
Ask the runtime what it sees
The REPL’s built-ins are often faster than searching documentation:
help(object_name) # rendered documentation and signature
type(object_name) # concrete runtime type
isinstance(value, Expected) # semantic type check
dir(object_name) # discover names; not a promise of public API
vars(object_name) # __dict__ when one exists
getattr(object_name, "name", None)
Use inspect when a callable or module is unfamiliar:
from inspect import getsource, signature
print(signature(function))
print(getsource(function))
Some built-ins, compiled extensions, and dynamically generated objects do not expose a retrievable signature or source. Catch that limitation instead of treating it as absence of behavior.
From the shell:
python -m pydoc pathlib.Path
python -c "import pandas; print(pandas.__file__)"
python -c "from importlib.metadata import version; print(version('pandas'))"
package.__file__ answers what code was imported; distribution metadata answers what package version was installed. They are related but not identical, especially for namespace packages and editable installs.
Reuse the standard library as command-line tools
Several standard-library modules are useful shell utilities, with no extra installation:
# Validate and pretty-print JSON
python -m json.tool input.json
# Inspect archives without extracting them
python -m zipfile -l results.zip
python -m tarfile -l results.tar.gz
# Precompile a tree and report syntax failures
python -m compileall -q src tests
# Serve one directory locally; do not expose it as a production server
python -m http.server 8000 --bind 127.0.0.1 --directory reports
Use python -m MODULE -h before assuming flags. Python’s list of modules with command-line interfaces grows over time, and module CLIs can differ between Python releases.
For a one-off data probe, keep the output narrow and deterministic:
python -c "import csv; rows=list(csv.DictReader(open('samples.csv', newline=''))); print(len(rows)); print(sorted(rows[0]))"
Once a probe becomes important or multiline, move it into a version-controlled script with named inputs, validation, and tests.
Test, debug, and profile in that order
Do not profile code that is not yet known to be correct. Escalate in this sequence:
# 1. Run focused tests
python -m pytest -q tests/test_manifest.py
python -m pytest -q -k sample_sheet
python -m unittest discover -s tests -v
python -m doctest -v src/example.py
# 2. Turn warnings into visible evidence
python -X dev -W default script.py
# 3. Step through control flow
python -m pdb script.py --input samples.csv
# 4. Benchmark a small expression
python -m timeit -s "x=list(range(10000))" "sum(x)"
# 5. Profile a real run
python -m cProfile -o profile.pstats script.py --input samples.csv
python -m pstats profile.pstats
# 6. Investigate memory allocation or low-level crashes
python -X tracemalloc=25 script.py
python -X faulthandler script.py
Inside pdb, the small working set is usually enough:
| Command | Action |
|---|---|
l |
Show source around the current line |
n |
Execute the next line in the current frame |
s |
Step into a call |
p expression |
Evaluate and print an expression |
pp expression |
Pretty-print an expression |
u / d |
Move up or down the stack |
c |
Continue to the next breakpoint |
q |
Quit |
Place breakpoint() in code when the suspicious location is already known. Remove it before unattended execution.
Use timeit for small, repeatable alternatives and cProfile for the shape of a real program. A profiler adds overhead; its result identifies candidates for measurement, not a standalone benchmark. For a hanging multithreaded process, faulthandler.dump_traceback_later() can emit every thread’s stack after a timeout.
Launch external tools without hiding failure
Bioinformatics Python often coordinates tools rather than replacing them. Pass an argument list, fail on non-zero exit, and make logging an explicit choice:
from subprocess import run
result = run(
["git", "rev-parse", "HEAD"],
check=True,
text=True,
capture_output=True,
)
commit = result.stdout.strip()
For a long Nextflow run, inherit the terminal streams or write them to a file rather than retaining all output in memory:
from pathlib import Path
from subprocess import STDOUT, run
Path("logs").mkdir(exist_ok=True)
with Path("logs/nextflow.log").open("w", encoding="utf-8") as log:
run(
[
"nextflow", "run", "nf-core/rnaseq",
"-profile", "docker",
"--input", "samplesheet.csv",
"--outdir", "results",
],
check=True,
stdout=log,
stderr=STDOUT,
)
Avoid shell=True unless shell syntax is the actual requirement and every interpolated value is trusted and quoted correctly. An argument list preserves boundaries, avoids accidental globbing, and records the command more faithfully. Add cwd=, env=, and timeout= only when the workflow contract requires them.
Keep a small command ladder
When a run fails, this order usually minimizes guesswork:
python -VVandpython -c "import sys; print(sys.executable)"python -m pip check- Import the failing package and print
__file__plus its distribution version. - Reproduce with the smallest focused test.
- Re-run under
-X dev, thenpdbif control flow is unclear. - Use
-X importtime,timeit,cProfile, ortracemalloconly for the resource that is actually abnormal. - Save the exact command, exit status, environment record, and relevant log with the analysis.
The principle is simple: interrogate before modifying. A precise command should reduce uncertainty; if it does not, it is probably too broad.