PythonRunning Python Code

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.

Bash
$ 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.

Tip
Run `python -i script.py` to execute a script and then drop into an interactive prompt afterwards, with all of the script's variables and functions still in scope. It's a fast way to inspect the end state of a program without adding print statements.
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).

Python
# hello.py
def greet(name):
    return f"Hello, {name}!"

if __name__ == "__main__":
    print(greet("World"))

Bash
$ 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.

Bash
# 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.

Note
For your own packages, `-m` lets a module use relative imports (things like `from . import helpers`) safely, because Python knows the module's full package context. Running the same file directly with `python mypackage/tool.py` often raises `ImportError: attempted relative import with no known parent package`, since the interpreter then treats the file as a standalone script with no package identity.
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.

Bash
# 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.