Python started as a scripting language. That fact is both its origin story and the source of its most persistent misconception. Three and a half decades later, the same language runs file-moving one-liners and distributed machine learning systems. The difference between those use cases is not the language — it is the architecture.
When Guido van Rossum began building Python during the Christmas holidays of 1989 at Centrum Wiskunde & Informatica (CWI) in the Netherlands, he was looking for a scripting language with clean syntax that could interface with the Amoeba operating system. He wanted something better than the ABC language he had been working with — something extensible, something practical. What he built was a language whose design, as the Python Wiki describes, was intended from the start to be "a small core language with a large standard library with an easily extensible interpreter," born from Van Rossum's frustrations with ABC's opposite approach.
According to the 2025 Stack Overflow Developer Survey, which collected responses from over 49,000 developers, Python saw a 7 percentage point increase in adoption from 2024 to 2025 — the largest single-year jump of any technology surveyed. The survey attributed this growth to Python's position as the go-to language for AI, data science, and back-end development. The JetBrains/PSF Python Developers Survey 2024, with over 30,000 respondents, paints an even more detailed picture: web development, data analysis, machine learning, DevOps automation, and scripting all compete for the top use case.
So what does it actually mean to use Python "for scripting," and how does that differ from every other way people use the language? This article walks through each mode of Python usage — with real code, the relevant PEPs, and the architectural trade-offs that matter.
What "Scripting" Actually Means
A script, in the traditional sense, is a program that automates a task that a human would otherwise perform manually. It is typically short, procedural, runs from top to bottom, and exits when done. Scripts do not serve incoming requests. They do not maintain persistent state between runs. They do not define APIs.
Here is a Python script in the purest sense:
import os
import shutil
from datetime import datetime
SOURCE = "/home/user/downloads"
ARCHIVE = "/home/user/archive"
today = datetime.now().strftime("%Y-%m-%d")
dest = os.path.join(ARCHIVE, today)
os.makedirs(dest, exist_ok=True)
for filename in os.listdir(SOURCE):
filepath = os.path.join(SOURCE, filename)
if os.path.isfile(filepath):
shutil.move(filepath, os.path.join(dest, filename))
print(f"Moved: {filename}")
print(f"Archive complete: {dest}")
That is a script. It moves files from one directory to another, organized by date. It runs once, does its job, and terminates. No classes, no frameworks, no event loops. Just imperative code that solves a problem.
Scripting in Python commonly takes the form of file manipulation (renaming, sorting, converting formats), data extraction and transformation (parsing CSV files, cleaning log output), system administration (managing processes, checking disk space, rotating logs), web scraping (pulling data from websites with requests and BeautifulSoup), and task glue (connecting one tool's output to another tool's input).
Wikipedia describes Python as being "often referred to as a 'glue language' because it is purposely designed to be able to integrate components written in other languages." The Python Wiki elaborates that because of "the wide variety of tools provided by the standard library, combined with the ability to use a lower-level language such as C and C++, Python can be a powerful glue language between languages and tools." This was always the intent — and it is exactly why the language spread so far beyond its original scope.
The if __name__ == "__main__" Boundary
The single most important line of code that separates a Python script from a Python module is this:
if __name__ == "__main__":
main()
This idiom exists because Python does not distinguish between "script files" and "library files" at the syntax level. Any .py file can be both imported as a module and executed as a script. When Python runs a file directly, it sets the special variable __name__ to "__main__". When the same file is imported by another module, __name__ is set to the module's actual name instead.
This duality was important enough to warrant formal attention from the Python Enhancement Proposal process. PEP 299, authored by Guido van Rossum himself, proposed replacing the if __name__ == "__main__" idiom with a dedicated __main__() function. The PEP argued that there should be "one simple and universal idiom for invoking a module as a standalone script" and that the existing pattern "is unclear to programmers of languages like C and C++." PEP 299 was ultimately rejected, but the discussion it sparked was significant because it acknowledged a fundamental tension in Python's design: the language treats scripts and modules as the same thing, and the community needed a clear convention to tell them apart.
PEP 338, written by Nick Coghlan and accepted for Python 2.5, took a different approach to the problem. Instead of changing the idiom, it enhanced Python's ability to execute modules as scripts using the -m command line switch. PEP 338 introduced the runpy module and extended -m to support packages, making it possible to execute any module as a script without knowing its filesystem path:
# Before PEP 338: you need the file path
python /usr/lib/python3/json/tool.py data.json
# After PEP 338: you use the module name
python -m json.tool data.json
PEP 338 formally recognized that the boundary between "script" and "module" is fluid. A well-written Python file should work as both. Structure your scripts with a main() function from the start — it costs nothing and makes the code immediately importable if you ever need it.
Web Development: Scripts That Never Stop Running
Web development is where Python most visibly stops being a scripting language. A web application is not a script that runs and exits — it is a long-running process that listens for requests, routes them, processes them, and returns responses. The architectural shift is fundamental.
The Python Developers Survey 2024 showed that 46% of Python developers use the language for web development, rebounding from a downward trend between 2021 and 2023. FastAPI was the biggest growth story, jumping from 29% to 38% adoption — a 30% increase in a single year. Django and Flask remain the established frameworks.
Here is what a minimal web application looks like in Flask:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/status")
def status():
return jsonify({"status": "healthy", "version": "1.2.0"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
And the equivalent in FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/status")
async def status():
return {"status": "healthy", "version": "1.2.0"}
Notice what is different from scripting. There is no sequential flow from top to bottom. The code defines routes — essentially callback functions — that respond to external events. The application's lifetime is indefinite. State management, database connections, authentication, and error handling become architectural concerns that do not exist in a 30-line file-moving script.
FastAPI's rise is directly connected to Python's async capabilities, which were formalized in PEP 3156 (Guido van Rossum's asyncio proposal, accepted into Python 3.4) and PEP 492 (which introduced the async and await keywords in Python 3.5). These PEPs transformed Python from a language where concurrency required threads or multiprocessing into one where a single thread could handle thousands of concurrent I/O operations — exactly what a web server needs.
"The proposal included a pluggable event loop, transport and protocol abstractions similar to those in Twisted, and a higher-level scheduler based on yield from." — Guido van Rossum, PEP 3156
Data Science and Machine Learning: Python as a Thinking Tool
If web development turned Python into a server-side language, data science turned it into a laboratory instrument.
The way Python is used in data science is fundamentally different from both scripting and web development. A data scientist using Python in a Jupyter notebook is not writing a program that runs from start to finish, nor a server that handles requests. They are engaged in interactive, exploratory computation — running code cell by cell, examining output, adjusting parameters, and iterating toward insight.
import pandas as pd
import matplotlib.pyplot as plt
# Load and explore
df = pd.read_csv("sales_data.csv")
print(df.describe())
# Transform
monthly = df.groupby(df["date"].str[:7])["revenue"].sum()
# Visualize
monthly.plot(kind="bar", figsize=(12, 6), title="Monthly Revenue")
plt.ylabel("Revenue ($)")
plt.tight_layout()
plt.savefig("monthly_revenue.png")
This code might live in a Jupyter notebook, where each section runs independently. The developer examines df.describe() before deciding how to group the data. They might change the aggregation from sum() to mean() and re-run just that cell. This iterative, conversational style of programming is closer to how a mathematician uses a calculator than how a software engineer writes an application.
The GitHub Octoverse 2024 report found that Python usage in Jupyter Notebooks surged by 92%, reflecting this pattern. Python's dominance in data science is not because it is the fastest language or the most elegant — it is because libraries like NumPy, pandas, and scikit-learn provide expressive interfaces that let domain experts (statisticians, biologists, economists) work with data without becoming full-time software engineers.
Machine learning extends this further. Training a neural network in PyTorch is not scripting, and it is not application development either. It is more like writing a specification for a mathematical process:
import torch
import torch.nn as nn
class SentimentClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.fc1 = nn.Linear(embed_dim, 128)
self.fc2 = nn.Linear(128, num_classes)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.3)
def forward(self, x):
x = self.embedding(x).mean(dim=1)
x = self.dropout(self.relu(self.fc1(x)))
return self.fc2(x)
This class does not "do" anything when you run it. It defines a computation graph. The actual work happens when you pass data through it during training. The mental model is declarative: you describe what the network looks like, and the framework handles execution.
Automation and DevOps: Scripting's Evolution
Automation is where the line between "scripting" and "software engineering" gets blurred in the most productive way.
A pure script automates a single task. An automation system orchestrates many tasks, handles failures, logs outcomes, and manages state across runs. Here is a script that checks whether a web service is up:
import requests
response = requests.get("https://api.example.com/health", timeout=10)
if response.status_code == 200:
print("Service is healthy")
else:
print(f"Service returned {response.status_code}")
And here is that same concern embedded in an automation system:
import requests
import logging
import smtplib
from email.message import EmailMessage
from datetime import datetime
logging.basicConfig(
filename="/var/log/healthcheck.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
ENDPOINTS = [
{"name": "API", "url": "https://api.example.com/health"},
{"name": "Auth", "url": "https://auth.example.com/health"},
{"name": "CDN", "url": "https://cdn.example.com/status"},
]
def check_endpoint(endpoint):
try:
resp = requests.get(endpoint["url"], timeout=10)
if resp.status_code == 200:
logging.info(f"{endpoint['name']}: healthy")
return True
else:
logging.warning(f"{endpoint['name']}: status {resp.status_code}")
return False
except requests.RequestException as e:
logging.error(f"{endpoint['name']}: unreachable - {e}")
return False
def send_alert(failures):
msg = EmailMessage()
msg["Subject"] = f"Health Check Alert - {len(failures)} service(s) down"
msg["From"] = "alerts@example.com"
msg["To"] = "ops@example.com"
body = f"Checked at {datetime.utcnow().isoformat()}Z\n\n"
body += "\n".join(f" - {f['name']}: {f['url']}" for f in failures)
msg.set_content(body)
with smtplib.SMTP("smtp.example.com", 587) as server:
server.starttls()
server.login("alerts@example.com", "token")
server.send_message(msg)
if __name__ == "__main__":
failures = [ep for ep in ENDPOINTS if not check_endpoint(ep)]
if failures:
send_alert(failures)
logging.critical(f"{len(failures)} endpoint(s) failed health check")
The second example is still a script — it runs and exits — but it has error handling, logging, configuration, and alerting. It is production automation rather than a quick hack. This is where many Python developers spend the plurality of their time: writing code that is too complex to be called a one-off script but does not constitute an "application" in any traditional sense.
CLI Tools and Libraries: Python as Infrastructure
Another distinct use of Python is building command-line tools and reusable libraries — code that other developers consume rather than end users.
A CLI tool is not a script, even though it runs from the terminal like one. The distinction is in the interface contract: a script is written for the author, while a CLI tool is written for other people. That difference drives significant design decisions around argument parsing, error messages, documentation, and exit codes.
import argparse
import sys
import csv
import json
def csv_to_json(input_path, output_path, indent=2):
with open(input_path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
records = list(reader)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(records, f, indent=indent, ensure_ascii=False)
return len(records)
def main():
parser = argparse.ArgumentParser(
description="Convert a CSV file to JSON format."
)
parser.add_argument("input", help="Path to the input CSV file")
parser.add_argument("output", help="Path for the output JSON file")
parser.add_argument(
"--indent", type=int, default=2,
help="JSON indentation level (default: 2)"
)
args = parser.parse_args()
try:
count = csv_to_json(args.input, args.output, args.indent)
print(f"Converted {count} records to {args.output}")
except FileNotFoundError:
print(f"Error: File '{args.input}' not found.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
This uses argparse from the standard library, but modern CLI tools increasingly use libraries like click or typer for richer interfaces. The code is structured around a user-facing contract: it has help text, error handling with proper exit codes, and a main() function that separates the entry point from the logic.
Writing reusable libraries is yet another mode entirely. Library code is not meant to be executed directly at all — it is meant to be imported. The concerns shift to API design, backward compatibility, documentation, type annotations, and packaging. PEP 8, adapted from Van Rossum's original Python Style Guide essay and co-authored by Barry Warsaw and Alyssa Coghlan, provides the naming conventions that make library code predictable: snake_case for functions and variables, CamelCase for classes, UPPER_CASE for constants.
"Code is read much more often than it is written." — Guido van Rossum, cited in PEP 8
That principle applies everywhere in Python, but it is especially critical in library code where other developers need to understand your API without reading the implementation.
The "Batteries Included" Philosophy and Its Limits
One reason Python works well across all these modes is the standard library. PEP 206, authored in 2000, formally articulated the "batteries included" philosophy: Python ships with "a rich and versatile standard library which is immediately available, without making the user download separate packages."
But PEP 206 was also remarkably candid about the standard library's limitations. It noted that "the standard library modules aren't always the best choices for a job. Some library modules were quick hacks, some were designed poorly and are now near-impossible to fix, and some have been rendered obsolete by other, more complete modules."
This tension played out over two decades. PEP 594, accepted in 2020, proposed removing "dead batteries" from the standard library — modules like cgi, aifc, and uu that had been superseded by better alternatives available through PyPI. The PEP referenced PEP 206's original criticism and argued that with modern package management (pip, PyPI, virtual environments), the cost of maintaining obsolete standard library modules outweighed the convenience of shipping them.
For scripting, the batteries-included approach is still a significant advantage. You can write a useful script using only the standard library: os and shutil for file operations, csv and json for data formats, argparse for CLI interfaces, logging for observability, sqlite3 for local databases, and http.server for quick-and-dirty web servers. No pip install required.
For application development, the standard library is a starting point but rarely sufficient. Web developers reach for Django or Flask. Data scientists reach for pandas and NumPy. ML engineers reach for PyTorch or TensorFlow. The standard library provides the foundation; the ecosystem builds the specialized tools.
So Which Mode Are You In?
The answer depends on what you are building and who it is for. Here is a framework for thinking about it:
- Scripting: The code runs once (or on a schedule), solves a specific task, is primarily used by you or your team, and does not need to handle external requests. Scripts tend to be single-file, procedural, and disposable. The quality bar is "does it work?" not "is it maintainable in two years?"
- Application development: The code runs continuously, handles external input (HTTP requests, user interactions, message queues), needs to be maintained by a team over time, and has reliability and scalability requirements. Applications have architecture: routing, middleware, database layers, configuration management, testing infrastructure.
- Data science: The code is exploratory and iterative. The goal is understanding rather than production deployment, and the workflow is interactive (notebooks, REPL sessions). Code quality matters less than analytical correctness.
- Infrastructure and tooling: The code is consumed by other developers (libraries, frameworks, CLI tools), the API surface is a first-class design concern, and backward compatibility and documentation are non-negotiable.
- Automation: The code sits somewhere between scripting and application development. It runs unattended, needs error handling and logging, but does not serve external requests. This is the middle ground where many developers spend the plurality of their time.
These categories are not mutually exclusive. A data scientist's exploratory notebook often gets refactored into an automation script, which gets wrapped in a FastAPI endpoint, which becomes part of a production application. Python's power is that the same language carries you through all of those transitions. You do not switch from R to Java to Go as your requirements evolve. You restructure your Python.
The Real Difference Is Architectural, Not Linguistic
Here is what many articles about "Python for scripting" get wrong: they treat it as a question about the language's capabilities, when it is actually a question about software architecture.
Python the language is identical whether you are writing a 15-line file-renaming script or a Django application serving millions of requests. The syntax is the same. The standard library is the same. The import system is the same. What changes is how you organize the code, how you handle failure, how you manage state, and how you design for change over time.
A script has no architecture. It is a sequence of steps. An application has architecture by necessity: it must handle concurrency, persistence, configuration, deployment, monitoring, and evolution. The difference is not in the language — it is in the engineering.
Van Rossum designed Python so that the distance between "quick script" and "production system" could be traveled incrementally. You start with a .py file. You add functions. You add a main() guard. You add argparse. You add error handling and logging. You split into modules. You add type hints. You add tests. At no point do you need to switch languages or rewrite from scratch. Each step is small, and each step makes the code more robust.
That incremental path from script to system — from "it works on my machine" to "it runs in production" — is Python's real superpower. Not the syntax. Not the libraries. The fact that you can start messy and clean up gradually, without ever hitting a wall that forces you to throw everything away and start over.
That is what makes Python Python. Not that it is good for scripting, or good for web development, or good for machine learning. It is good for growing your code alongside your understanding of the problem.
Real code. Real examples. Real understanding. That is the promise, and that is what we deliver.