Running Python Code
Before you can write real programs, you need to know how Python code actually gets executed. Unlike compiled languages where you build a binary once and run it forever, Python offers several distinct ways to run the same code, and each one is suited to a different kind of task. Picking the right mode isn't just a matter of taste — it changes how you debug, how you share your work, and how much state you carry between commands.
In this guide we'll walk through the three core ways to run Python (the
interactive REPL, script files, and modules via -m), then look at the -c flag
for quick one-off snippets, and finally take a short detour into Jupyter notebooks,
which use a completely different execution model built around cells rather than a
single top-to-bottom pass.
The interactive REPL
The simplest way to run Python is to type python (or python3 on many
Linux and macOS systems) with no arguments at all. This drops you into the
REPL — Read, Evaluate, Print, Loop — an interactive shell that reads one
expression or statement at a time, evaluates it immediately, prints the result,
and waits for the next line.
$ python
Python 3.12.3 (main, Apr 9 2024, 08:00:00)
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 + 2
4
>>> name = "Ada"
>>> print(f"Hello, {name}!")
Hello, Ada!
>>> exit()The REPL is ideal for quick experiments: checking how a function behaves, testing
a regular expression, or poking at an object's attributes with dir(). Every
variable you define stays alive for the rest of the session, which makes it a
great scratchpad — but that same statefulness is a liability for anything you
want to reproduce later, since the exact sequence of commands you typed usually
isn't saved anywhere.
Running a script file
For anything beyond a one-off experiment, you'll write your code into a
.py file and run it as a script. This is the mode you'll use for the vast
majority of real programs: the interpreter reads the file top to bottom,
executing each statement in order, and exits when it reaches the end (or hits
an unhandled exception).
# hello.py
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
print(greet("World"))$ python hello.py
Hello, World!
Scripts are deterministic and repeatable: anyone with the same file and the same
interpreter gets the same result, which is exactly what you want for anything you
plan to commit to version control, schedule as a cron job, or hand off to a
teammate. The if __name__ == "__main__": guard is a common convention that lets
a file be both a runnable script and an importable module without the
top-level code firing on import.
Running a module with -m
The -m flag tells Python to locate a module using its normal import machinery
(searching sys.path, respecting packages) and then execute it as if it were the
top-level script, rather than pointing directly at a file path. This distinction
matters more than it sounds: -m resolves the module the same way import would,
so it correctly handles modules that live inside packages and rely on relative
imports, which running the file directly often breaks.
# Start a simple static file server on port 8000 python -m http.server 8000 # Create a new virtual environment named "myenv" python -m venv myenv # Run pip as a module (useful when several Python versions are installed) python -m pip install requests # Run a package's own __main__.py as an application python -m mypackage
A lot of Python's standard library ships with runnable modules exactly for this
purpose — http.server, venv, json.tool, timeit, and unittest are all
designed to be invoked with -m. It's also the recommended way to run tools like
pip when you have multiple Python installations, because python -m pip
guarantees you're using the pip associated with that specific interpreter,
rather than whichever pip happens to be first on your PATH.
Quick one-liners with -c
Sometimes you don't want a REPL session or a saved file at all — you just want
to run a single expression and see the result, often from inside a shell script
or a Makefile. The -c flag takes a string of Python code and executes it
directly, printing nothing automatically (unlike the REPL) unless you call
print() yourself.
# Check the interpreter version programmatically
python -c "import sys; print(sys.version)"
# Quickly pretty-print a JSON file from the command line
python -c "import json; print(json.load(open('data.json')))"
# One-off math without opening a calculator
python -c "print(355 / 113)"3.14159292...
-c snippets are especially handy in shell pipelines and CI scripts where
spinning up a whole file just for a single check would be overkill. Because the
code is a plain string, multiple statements are separated with semicolons rather
than newlines, which keeps things readable only up to a point — once a one-liner
needs a loop or a conditional, it's usually a sign you should reach for a real
script instead.
A different model: Jupyter notebooks
Jupyter notebooks (.ipynb files) run Python through a fundamentally different
execution model. Instead of one linear pass from the top of a file to the bottom,
a notebook is a sequence of independently runnable cells, each of which can be
executed, re-executed, or skipped in any order you like, with all cells sharing
one long-lived kernel process and its state. This makes notebooks excellent for
exploratory data analysis: you load a dataset once in an early cell, then try a
dozen different transformations and plots in later cells without reloading
anything, seeing rich output (tables, charts, images) rendered inline after each
cell.
The trade-off is reproducibility. Because cells can be run out of order, a
notebook's visible outputs don't always reflect what would happen if you ran it
fresh from top to bottom — a classic source of "works on my notebook" bugs. For
production code, scheduled jobs, or anything that needs to run identically every
time, a plain script (or a module invoked with -m) is still the safer choice;
notebooks shine specifically in the exploratory, iterative phase of a project
before that logic gets consolidated into real .py files.
Choosing the right mode
Mode | Best Use Case | State Persistence | Shareability | Typical Audience |
|---|---|---|---|---|
REPL | Quick experiments, checking syntax, exploring an API | Lives only for the session; lost on exit | Poor — no file is saved by default | Anyone debugging or learning interactively |
Script (.py) | Production code, automation, CLI tools, scheduled jobs | None between runs; each run starts fresh | Excellent — a single file, easy to version control | Developers, ops, anyone running repeatable programs |
Notebook (.ipynb) | Exploratory data analysis, prototyping, teaching | Persists across cells within one kernel session | Good for sharing narrative + output, weaker for reuse as code | Data scientists, researchers, analysts |
As a rule of thumb: reach for the REPL or -c when a question can be answered
in a single line, write a script the moment logic needs to run the same way more
than once, use -m whenever you're invoking a module rather than a standalone
file (especially anything from the standard library or inside your own
packages), and save notebooks for the messy, iterative exploration that precedes
a clean script.