Why Python

Guido van Rossum started Python in 1989 as a hobby project over Christmas break. He wanted a language that was easy to read, easy to teach, and powerful enough to get real work done. He named it after Monty Python. Thirty-five years later, it is the most widely taught programming language in the world and the backbone of the artificial intelligence revolution.

Python’s design philosophy is captured in a document called The Zen of Python. You can read it by typing import this in a Python interpreter. The most important lines:

“Readability counts.”

— The Zen of Python

“Simple is better than complex. Complex is better than complicated.”

— The Zen of Python

Python achieves readability through significant whitespace (indentation is syntax), minimal punctuation (no semicolons, no braces for blocks), and a standard library so comprehensive it is described as “batteries included.” The result is a language where a beginner can write useful programs on day one and an expert can build production systems without fighting the language.

I. Hello, World

print("Hello, World")

That is the entire program. No imports, no main function, no class wrapper, no semicolons. Save it as hello.py and run it:

$ python3 hello.py Hello, World

Or use the interactive REPL:

$ python3 >>> 2 + 2 4 >>> "hello".upper() 'HELLO' >>> [x**2 for x in range(5)] [0, 1, 4, 9, 16]

The REPL (Read-Eval-Print Loop) is one of Python’s best features for learning and experimentation. Type an expression, see the result immediately. No compilation step, no boilerplate. This instant feedback loop is why Python is the most popular first language in computer science education.

II. Variables and Types

Python is dynamically typed. You do not declare types; the interpreter infers them at runtime.

x = 42 # int pi = 3.14159 # float name = "Python" # str active = True # bool nothing = None # NoneType (Python's null) # Variables can change type (duck typing) x = "now I'm a string" x = [1, 2, 3] # now a list # Check types print(type(x)) # <class 'list'> print(isinstance(x, list)) # True

Python follows “duck typing”: if it walks like a duck and quacks like a duck, it is a duck. The interpreter does not care what type a variable is. It cares whether the variable supports the operation you are performing on it. Call .upper() on anything that has an upper method; it does not matter whether it is a str or a custom class.

Numeric Types

# Integers have arbitrary precision (no overflow!) big = 2 ** 1000 # works fine, hundreds of digits # Float division vs integer division 7 / 2 # 3.5 (true division) 7 // 2 # 3 (floor division) 7 % 2 # 1 (modulo) # Exponentiation 2 ** 10 # 1024 # Complex numbers are built in z = 3 + 4j z.real # 3.0 z.imag # 4.0 abs(z) # 5.0 (magnitude)

Python integers never overflow. They grow to arbitrary size. This is unusual among programming languages and means you never need to worry about integer overflow bugs. The tradeoff is performance: big-integer arithmetic is slower than fixed-width integers in C or Go.

III. Strings

# String literals (single or double quotes, no difference) s1 = 'hello' s2 = "hello" # Triple quotes for multi-line strings s3 = """This is a multi-line string.""" # f-strings (Python 3.6+): the best string formatting name = "World" n = 42 print(f"Hello, {name}! The answer is {n}.") print(f"Two plus two is {2 + 2}") # expressions work print(f"Pi is {3.14159:.2f}") # "Pi is 3.14" # Slicing (works like Go/Rust, but supports negative indices) s = "Hello, World" s[0:5] # "Hello" s[7:] # "World" s[:5] # "Hello" s[-5:] # "World" (negative indices count from the end) s[::-1] # "dlroW ,olleH" (reverse) # Strings are immutable # s[0] = "h" # TypeError! # Common methods "hello world".split() # ["hello", "world"] ", ".join(["a", "b", "c"]) # "a, b, c" " hello ".strip() # "hello" "hello".replace("l", "r") # "herro" "hello".startswith("he") # True "42".isdigit() # True

f-strings are Python’s best feature for string formatting. They are readable, fast, and support arbitrary expressions inside the braces. Before f-strings, Python had % formatting and .format(); both still work, but f-strings are superior in almost every case.

IV. Data Structures

Lists

Python lists are dynamic arrays (like Java’s ArrayList or Go’s slices). They are ordered, mutable, and can hold mixed types (though you usually keep them homogeneous).

# Creating lists nums = [1, 2, 3, 4, 5] empty = [] mixed = [1, "two", 3.0, True] # Indexing and slicing (same as strings) nums[0] # 1 nums[-1] # 5 nums[1:3] # [2, 3] # Mutating nums.append(6) # [1, 2, 3, 4, 5, 6] nums.extend([7, 8]) # [1, 2, 3, 4, 5, 6, 7, 8] nums.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6, 7, 8] nums.pop() # removes and returns 8 nums.remove(3) # removes first occurrence of 3 # Sorting nums.sort() # in-place sorted(nums, reverse=True) # returns new sorted list sorted(words, key=len) # sort by length # Useful operations len(nums) # length 3 in nums # membership test: True sum(nums) # sum of all elements min(nums), max(nums) # min and max

Tuples

Tuples are immutable lists. Use them when the data should not change: coordinates, return values, dictionary keys.

# Creating tuples point = (3, 4) rgb = (255, 128, 0) single = (42,) # trailing comma required for single-element tuples # Unpacking (one of Python's best features) x, y = point # x = 3, y = 4 r, g, b = rgb # r = 255, g = 128, b = 0 # Swap without a temp variable a, b = b, a # Unpacking in loops pairs = [("Alice", 30), ("Bob", 25), ("Carol", 35)] for name, age in pairs: print(f"{name} is {age}")

Dictionaries

Dictionaries are hash maps. They are Python’s most important data structure after lists.

# Creating dicts ages = {"Alice": 30, "Bob": 25, "Carol": 35} empty = {} # Access ages["Alice"] # 30 ages.get("Eve", 0) # 0 (default if missing; avoids KeyError) # Insert / update ages["Dave"] = 28 # Delete del ages["Bob"] # Iteration for name in ages: # keys print(name) for name, age in ages.items(): # key-value pairs print(f"{name}: {age}") for age in ages.values(): # values only print(age) # Check membership "Alice" in ages # True # Dict comprehension squares = {x: x**2 for x in range(6)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Sets

# Creating sets primes = {2, 3, 5, 7, 11} evens = {2, 4, 6, 8, 10} # Set operations primes & evens # {2} — intersection primes | evens # {2, 3, 4, 5, 6, 7, 8, 10, 11} — union primes - evens # {3, 5, 7, 11} — difference primes ^ evens # {3, 4, 5, 6, 7, 8, 10, 11} — symmetric difference # Membership testing (O(1) average, unlike lists which are O(n)) 7 in primes # True # Deduplication list(set([1, 2, 2, 3, 3, 3])) # [1, 2, 3]

V. Control Flow

# if / elif / else if x > 0: print("positive") elif x == 0: print("zero") else: print("negative") # for loops for i in range(5): # 0, 1, 2, 3, 4 print(i) for i in range(2, 10, 3): # 2, 5, 8 (start, stop, step) print(i) # enumerate: index + value (much better than range(len(...))) fruits = ["apple", "banana", "cherry"] for i, fruit in enumerate(fruits): print(f"{i}: {fruit}") # zip: iterate over multiple sequences in parallel names = ["Alice", "Bob"] scores = [95, 87] for name, score in zip(names, scores): print(f"{name}: {score}") # while loop while x > 0: x -= 1

The for/else Clause

This is unusual. Python’s for loop has an optional else clause that runs only if the loop completed without hitting a break.

# Search for a prime factor n = 97 for i in range(2, int(n**0.5) + 1): if n % i == 0: print(f"{n} is divisible by {i}") break else: # This runs if the loop did NOT break print(f"{n} is prime")

Match/Case (Python 3.10+)

match command: case "quit": print("Goodbye") case "help": show_help() case ["go", direction]: # destructuring! move(direction) case {"action": action, "target": target}: # dict matching! perform(action, target) case _: print("Unknown command")

Python’s match is more powerful than a switch statement. It does structural pattern matching: you can match on types, destructure sequences and dictionaries, and bind variables in the match arms.

VI. Functions

# Basic function def add(a, b): return a + b # Default arguments def greet(name, greeting="Hello"): return f"{greeting}, {name}!" greet("Alice") # "Hello, Alice!" greet("Alice", "Hey") # "Hey, Alice!" # *args and **kwargs def flexible(*args, **kwargs): print(f"args: {args}") # tuple of positional args print(f"kwargs: {kwargs}") # dict of keyword args flexible(1, 2, 3, x=10, y=20) # args: (1, 2, 3) # kwargs: {'x': 10, 'y': 20} # Type hints (optional, not enforced at runtime) def divide(a: float, b: float) -> float: return a / b # Lambda (anonymous functions) square = lambda x: x ** 2 sorted(words, key=lambda w: len(w))

Docstrings

def binary_search(arr, target): """Search for target in a sorted array. Args: arr: A sorted list of comparable elements. target: The element to search for. Returns: The index of target, or -1 if not found. """ lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid elif arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1

Docstrings are the first string literal in a function, class, or module. They become the __doc__ attribute and are used by help() and documentation generators. Use them.

VII. Comprehensions and Generators

Comprehensions are Python’s most elegant feature. They replace explicit loops with concise, readable expressions.

# List comprehension squares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # With a filter evens = [x for x in range(20) if x % 2 == 0] # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # Nested comprehension (flatten a matrix) matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flat = [x for row in matrix for x in row] # [1, 2, 3, 4, 5, 6, 7, 8, 9] # Dict comprehension word_lengths = {w: len(w) for w in ["hello", "world", "python"]} # {'hello': 5, 'world': 5, 'python': 6} # Set comprehension unique_lengths = {len(w) for w in ["hello", "world", "python"]} # {5, 6}

Generators

Generators produce values lazily, one at a time, without building the entire list in memory. Use them when the sequence is large or infinite.

# Generator expression (parentheses instead of brackets) total = sum(x**2 for x in range(1_000_000)) # Computes without building a million-element list # Generator function (yield instead of return) def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b # Take the first 10 Fibonacci numbers from itertools import islice list(islice(fibonacci(), 10)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # Read a huge file line by line (memory efficient) def grep(pattern, filename): with open(filename) as f: for line in f: # file objects are iterators if pattern in line: yield line.strip()

The difference matters when the data is large. A list comprehension [x**2 for x in range(10_000_000)] allocates a list of ten million integers. A generator expression (x**2 for x in range(10_000_000)) produces them one at a time, using constant memory.

VIII. Decorators

A decorator is a function that takes a function and returns a modified version of it. The @ syntax is syntactic sugar for wrapping a function at definition time.

import time import functools # A timing decorator def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) elapsed = time.time() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper @timer def slow_function(): time.sleep(1) return "done" slow_function() # prints "slow_function took 1.0012s"

Built-in Decorators

# @functools.lru_cache — automatic memoization import functools @functools.lru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2) fib(100) # instant (without cache: heat death of the universe) # @property — make a method look like an attribute class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return 3.14159 * self.radius ** 2 c = Circle(5) c.area # 78.53975 — accessed like an attribute, computed like a method

IX. Classes

class Point: def __init__(self, x, y): self.x = x self.y = y def distance_to(self, other): return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5 def __repr__(self): return f"Point({self.x}, {self.y})" def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __eq__(self, other): return self.x == other.x and self.y == other.y p1 = Point(3, 4) p2 = Point(1, 2) print(p1) # Point(3, 4) print(p1 + p2) # Point(4, 6) print(p1.distance_to(p2)) # 2.828...

The __dunder__ methods (double underscore, or “dunder” methods) let you define how your objects behave with built-in operations. __repr__ controls what print() shows. __add__ controls the + operator. __len__ controls len(). __getitem__ controls [] indexing. This is how Python achieves operator overloading.

Dataclasses

For simple data-holding classes, @dataclass eliminates boilerplate:

from dataclasses import dataclass @dataclass class Point: x: float y: float def distance_to(self, other): return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5 # __init__, __repr__, __eq__ are generated automatically p = Point(3, 4) print(p) # Point(x=3, y=4)

Inheritance

class Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError class Dog(Animal): def speak(self): return f"{self.name} says woof!" class Cat(Animal): def speak(self): return f"{self.name} says meow!" animals = [Dog("Rex"), Cat("Whiskers")] for a in animals: print(a.speak())

X. Error Handling

# try / except / else / finally try: result = 10 / 0 except ZeroDivisionError as e: print(f"Error: {e}") except (TypeError, ValueError) as e: print(f"Bad input: {e}") else: # runs only if no exception was raised print(f"Result: {result}") finally: # always runs print("Done") # Raising exceptions def validate_age(age): if age < 0: raise ValueError(f"Age cannot be negative: {age}") return age # Custom exceptions class NotFoundError(Exception): def __init__(self, item): super().__init__(f"{item} not found") self.item = item

Context Managers (the with Statement)

# File handling: the with statement guarantees cleanup with open("data.txt", "r") as f: content = f.read() # f is automatically closed here, even if an exception occurred # Multiple context managers with open("input.txt") as src, open("output.txt", "w") as dst: dst.write(src.read().upper()) # Writing your own context manager from contextlib import contextmanager @contextmanager def timer(label): start = time.time() yield print(f"{label}: {time.time() - start:.4f}s") with timer("processing"): do_something_slow()

The with statement is Python’s equivalent of Go’s defer or Java’s try-with-resources. It guarantees that resources (files, database connections, locks) are properly cleaned up. Always use with for files. Never use f = open(...) without it.

XI. Modules and Packages

# Importing import os import json from pathlib import Path from collections import Counter, defaultdict import numpy as np # aliasing from datetime import datetime as dt # The __name__ guard if __name__ == "__main__": # This block runs only when the file is executed directly, # not when it is imported as a module main()

Virtual Environments

# Create a virtual environment $ python3 -m venv myenv # Activate it $ source myenv/bin/activate # macOS/Linux $ myenv\Scripts\activate # Windows # Install packages (isolated from the system Python) $ pip install requests flask pandas # Save dependencies $ pip freeze > requirements.txt # Reproduce the environment elsewhere $ pip install -r requirements.txt # Deactivate $ deactivate

Always use virtual environments. Never install packages into the system Python. This isolates project dependencies and prevents version conflicts. Every Python project should have its own virtual environment.

XII. The Standard Library

ModuleWhat It Does
os, sysOS interaction, environment variables, command-line args, file paths
pathlibModern path manipulation (prefer over os.path)
jsonJSON encoding/decoding. json.dumps(), json.loads()
collectionsCounter, defaultdict, deque, namedtuple, OrderedDict
itertoolschain, product, permutations, combinations, islice, groupby
functoolslru_cache, reduce, partial, wraps
reRegular expressions. re.search(), re.findall(), re.sub()
datetimeDates, times, timezones, formatting, arithmetic
typingType hints: List, Dict, Optional, Union, Callable, TypeVar
unittest, pytestTesting frameworks (pytest is third-party but universal)
loggingStructured logging with levels, handlers, formatters
http.serverSimple HTTP server in one line: python3 -m http.server
sqlite3Built-in SQLite database interface
argparseCommand-line argument parsing
csvCSV file reading and writing
math, statisticsMathematical functions, basic statistics

collections.Counter

from collections import Counter words = "the cat sat on the mat the cat".split() counts = Counter(words) print(counts) # Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1}) print(counts.most_common(2)) # [('the', 3), ('cat', 2)]

XIII. Python for Data

Python dominates data science and machine learning because of four libraries:

LibraryWhat It Does
NumPyN-dimensional arrays and vectorized math. The foundation of everything below. C performance with Python syntax.
pandasDataFrames: labeled, indexed, SQL-like tables. read_csv, groupby, merge, pivot. The workhorse of data analysis.
matplotlib / seabornPlotting: line charts, scatter plots, histograms, heatmaps. Publication-quality figures.
scikit-learnMachine learning: regression, classification, clustering, preprocessing, model selection. Consistent API for everything.
import numpy as np # NumPy: vectorized operations (no loops needed) a = np.array([1, 2, 3, 4, 5]) print(a * 2) # [2 4 6 8 10] print(a ** 2) # [ 1 4 9 16 25] print(np.mean(a)) # 3.0 print(np.std(a)) # 1.414... # Matrix operations A = np.array([[1, 2], [3, 4]]) B = np.array([[5, 6], [7, 8]]) print(A @ B) # matrix multiplication
import pandas as pd # pandas: DataFrames df = pd.read_csv("data.csv") df.head() # first 5 rows df.describe() # summary statistics df.groupby("category")["price"].mean() # average price per category df[df["price"] > 100] # filter rows

The AI revolution runs on Python. GPT, DALL-E, Stable Diffusion, AlphaFold — all trained using PyTorch or TensorFlow, both of which have Python as their primary interface. If you want to work in machine learning, you will work in Python. There is no serious alternative.

XIV. Putting It Together

Here is a complete program that demonstrates comprehensions, generators, dictionaries, Counter, error handling, context managers, and formatted output. It reads a text file, counts word frequencies, and reports the results.

import sys import re from collections import Counter from pathlib import Path def extract_words(text): """Extract lowercase words from text, stripping punctuation.""" return re.findall(r"[a-z']+", text.lower()) def analyze_file(path): """Read a file and return word frequency analysis.""" try: with open(path, "r", encoding="utf-8") as f: text = f.read() except FileNotFoundError: print(f"Error: '{path}' not found", file=sys.stderr) return None except UnicodeDecodeError: print(f"Error: '{path}' is not a text file", file=sys.stderr) return None words = extract_words(text) counts = Counter(words) return { "path": path, "total_words": len(words), "unique_words": len(counts), "top_20": counts.most_common(20), "avg_word_length": sum(len(w) for w in words) / len(words) if words else 0, "longest_words": sorted(set(words), key=len, reverse=True)[:5], } def print_report(analysis): """Print a formatted report of the word frequency analysis.""" if analysis is None: return print(f"\n{'=' * 50}") print(f" Word Frequency Analysis: {analysis['path']}") print(f"{'=' * 50}") print(f" Total words: {analysis['total_words']:,}") print(f" Unique words: {analysis['unique_words']:,}") print(f" Avg word length: {analysis['avg_word_length']:.1f}") print(f" Longest words: {', '.join(analysis['longest_words'])}") print(f"\n Top 20 words:") print(f" {'Word':20} {'Count':>6} {'Bar'}") print(f" {'-' * 40}") max_count = analysis["top_20"][0][1] if analysis["top_20"] else 1 for word, count in analysis["top_20"]: bar = "#" * int(20 * count / max_count) print(f" {word:20} {count:6,} {bar}") if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python wordcount.py <file> [file ...]") sys.exit(1) for filepath in sys.argv[1:]: analysis = analyze_file(filepath) print_report(analysis)
================================================== Word Frequency Analysis: hamlet.txt ================================================== Total words: 31,956 Unique words: 4,815 Avg word length: 4.1 Longest words: undiscovered, understanding, entertainment, extraordinary, communication Top 20 words: Word Count Bar ---------------------------------------- the 1,145 #################### and 967 ################ to 742 ############ of 673 ########### a 546 ######### i 542 ######### my 514 ######## in 436 ####### it 419 ####### you 415 ####### that 391 ###### is 346 ###### his 335 ##### not 316 ##### this 306 ##### with 291 ##### but 271 #### for 248 #### he 239 #### your 237 ####

What Python Trades Away

What You LoseWhat You Get
Speed (10–100x slower than C)Development speed (write programs in hours, not days)
Static type checkingFlexibility, rapid prototyping, duck typing
Compiled binariesInstant edit-run cycle, no compilation step
Low-level memory controlAutomatic garbage collection, no segfaults
True parallelism (the GIL)Simple concurrency model, multiprocessing as alternative

The Global Interpreter Lock (GIL) means only one thread can execute Python bytecode at a time. This makes CPU-bound multithreading useless in Python. For CPU-bound parallelism, use the multiprocessing module or write the hot loop in C/Rust. For I/O-bound concurrency (web requests, file I/O), threads and asyncio work fine because the GIL is released during I/O operations.

Python is slow. This is not a secret. But for most applications, it does not matter. If your program spends 90% of its time waiting for network responses, database queries, or user input, the speed of the language is irrelevant. And when performance does matter, Python calls into C: NumPy, pandas, PyTorch, and most performance-critical libraries are written in C or C++ with Python bindings. You get C speed with Python syntax.

Summary

Python is not trying to be the fastest language, or the most type-safe, or the most elegant. It is trying to be the most useful language for the widest range of people and problems. Its design can be summarized in a few principles:

  • Readability is not optional. Indentation is syntax. There is one obvious way to do things. Code is read more often than it is written.
  • Batteries included. The standard library handles JSON, HTTP, databases, regex, CSV, dates, logging, and testing without external dependencies.
  • Duck typing. If it works, it works. The type system gets out of your way. Type hints exist for documentation and tooling, not enforcement.
  • Rapid prototyping. The REPL, dynamic typing, and minimal boilerplate mean you can go from idea to working code in minutes.
  • Ecosystem. PyPI has over 500,000 packages. Whatever you need to build — a web app, a data pipeline, a machine learning model, a CLI tool, a game, a script to rename 10,000 files — someone has already built a library for it.

Python is the language you learn in a weekend and use for a career. It is the language of AI research, data science, web backends, DevOps automation, scientific computing, and introductory computer science courses. It is not the right tool for every job. But it is the right first tool for almost every programmer, and the right only tool for a surprising number of problems.