Python From Scratch - The Language That Reads Like English
Helpful context:
In 1991, a Dutch programmer named Guido van Rossum released a language he had been building in his spare time over the Christmas holiday. He named it after Monty Python’s Flying Circus, not the snake. He wanted it to be fun. He wanted it to be readable. He wanted it to feel like executable pseudocode - a language where the code said what it meant.
For about a decade, Python was a pleasant niche language used by scientists, educators, and people who found Perl too cryptic. Then something interesting happened. NumPy arrived in the mid-2000s. The machine learning boom of the 2010s needed a scripting layer flexible enough to glue together C++ numerical kernels. Python was already there, with the right properties, at the right time. By 2019 it had become the most-used language on GitHub. By 2024 it was the most-used language in the world.
Understanding how Python won - and what it gave up to win - is a better foundation for learning it than any list of syntax rules.
What Python Chose to Optimize For
Every language is a set of tradeoffs. Python made a specific set of choices, and they compound.
Readability as a design constraint. Python uses indentation to define code structure. There are no curly braces to argue about. An if block looks like an if block visually, not just syntactically. Guido called this “executable pseudocode” - and the analogy holds. When you read Python code, the logic is visible in the whitespace.
Dynamic typing. You do not declare the types of variables. The interpreter figures it out. This makes code shorter and exploration faster: you can try things in a REPL without writing type signatures. The cost is that type errors arrive at runtime rather than at compile time, and large codebases can develop subtle bugs where a variable holds the wrong kind of thing.
An enormous standard library and ecosystem. Python ships with batteries included - a large standard library covering HTTP, files, CSV, JSON, dates, math, and more. Beyond that, the Python Package Index (PyPI) contains hundreds of thousands of third-party packages. Whatever you want to do, someone has probably already built a library for it.
Interpreted execution. Python code is not compiled to machine code before running. The CPython interpreter reads your source, compiles it to bytecode, and executes that. This makes the iteration loop fast - write, run, observe - but it makes Python substantially slower than compiled languages for CPU-intensive work.
These choices explain both why Python is so popular and why it is not used everywhere. It is the right tool when readability, iteration speed, and ecosystem access matter more than raw performance.
The Part People Do Not Talk About: The GIL
There is a design decision inside CPython called the Global Interpreter Lock - the GIL. It is a mutex that ensures only one thread executes Python bytecode at a time. Even on a 16-core machine, multithreaded Python code typically runs on one core.
The GIL exists because CPython’s memory management is not thread-safe. Rather than redesign memory management, the original implementors added a global lock. This was pragmatic in 1991 and has been a source of frustration ever since.
The practical consequence: Python threads are good for I/O-bound work (waiting for network responses, waiting for disk reads) because threads release the GIL during I/O. They are useless for CPU-bound parallelism. For CPU parallelism, you use the multiprocessing module, which runs multiple Python interpreter processes rather than threads - heavier weight, but actually parallel.
Python 3.13 introduced an experimental “no-GIL” build, and Python 3.14 is expected to make it more stable. This is a live debate in the Python community. For now, if you are writing performance-critical parallel code, Python is the wrong language. If you are doing data science with NumPy, the GIL mostly does not matter because NumPy operations release it and are implemented in C.
Variables and Types
Python is dynamically typed - variables do not have types, values do:
x = 10 # int
price = 3.99 # float
name = "Alice" # str
active = True # bool
The same variable can hold different types at different times. This is a feature and a footgun. It makes exploration easy; it makes large codebases hard to reason about without a type checker like mypy.
The four primitive types you will use constantly:
int- arbitrary precision integers. Python integers do not overflow.float- 64-bit IEEE 754 floating point.0.1 + 0.2 != 0.3in Python just as in every other language.str- immutable Unicode strings. Python 3 strings are always Unicode.bool-TrueandFalse, which are literally subclasses ofint.
F-strings are the modern way to embed values in strings - cleaner than .format() or % interpolation:
name = "Bob"
age = 30
print(f"My name is {name} and I am {age} years old.")
# you can put any expression inside the braces
print(f"Next year I will be {age + 1}.")
Duck Typing: The Python Philosophy of Types
Here is a question that Python answers very differently from Java or C++: what is the “type” of a function argument?
Python’s answer is: it does not matter what type it is, only whether it has the methods or attributes you need. This is called duck typing - if it walks like a duck and quacks like a duck, treat it like a duck.
def total_length(items):
return sum(len(item) for item in items)
total_length(["hello", "world"]) # works - list of strings
total_length(("foo", "bar", "baz")) # works - tuple of strings
total_length({"one", "two"}) # works - set of strings
total_length does not check whether items is a list, a tuple, or a set. It just needs something iterable that contains things with a len. Any object that satisfies those properties works.
This is liberating for writing generic code. It is also how Python achieves polymorphism without requiring inheritance hierarchies. The downside is that type errors become visible only when the wrong type actually arrives at runtime - which is why type hints (introduced in Python 3.5) and tools like mypy have become important for larger projects.
Built-in Data Structures
Python’s four core collections are genuinely well-designed and cover most use cases:
List - ordered, mutable, O(1) append, O(n) search:
fruits = ["apple", "banana", "cherry"]
fruits.append("mango")
fruits.insert(0, "avocado") # insert at position
print(fruits[0]) # "avocado"
print(fruits[-1]) # "mango" - negative indexing from the end
print(fruits[1:3]) # ["banana", "cherry"] - slicing
Tuple - ordered, immutable. Use when the data should not change:
point = (3, 4)
x, y = point # unpacking - works for any iterable
rgb = (255, 128, 0)
Tuples are faster than lists for iteration and can be used as dictionary keys (because they are hashable). They signal intent: this collection is not going to change.
Dict - key-value mapping, O(1) average lookup. The workhorse of Python:
scores = {"Alice": 95, "Bob": 87}
scores["Carol"] = 91
print(scores.get("Dave", 0)) # 0 - default if key missing
for name, score in scores.items():
print(f"{name}: {score}")
Since Python 3.7, dicts maintain insertion order. This was an implementation detail that became guaranteed behavior.
Set - unordered, unique elements, O(1) membership test:
tags = {"python", "beginner", "python"} # duplicate removed automatically
print("python" in tags) # True - fast, unlike lists
print(tags & {"python", "advanced"}) # intersection: {"python"}
Control Flow
Python’s control flow is readable because the syntax matches how you would describe the logic in English:
# if/elif/else
grade = 85
if grade >= 90:
rating = "A"
elif grade >= 80:
rating = "B"
elif grade >= 70:
rating = "C"
else:
rating = "F"
for iterates over any iterable - lists, strings, dicts, ranges, files:
for fruit in ["apple", "banana", "cherry"]:
print(fruit)
for i in range(10): # 0 through 9
print(i)
for i, fruit in enumerate(["apple", "banana"]):
print(f"{i}: {fruit}") # 0: apple, 1: banana
while runs until a condition becomes false:
count = 0
while count < 3:
print(count)
count += 1
Use break to exit a loop immediately. Use continue to skip to the next iteration. Use else on a loop - it runs if the loop completed without hitting break, which is genuinely useful for search loops.
List Comprehensions
Comprehensions are one of Python’s nicest features - compact expressions for building collections from other iterables:
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
words = ["hello", "world", "foo"]
upper = [w.upper() for w in words if len(w) > 3]
# ["HELLO", "WORLD"]
Dict and set comprehensions follow the same pattern:
word_lengths = {word: len(word) for word in ["apple", "banana", "cherry"]}
# {"apple": 5, "banana": 6, "cherry": 6}
Comprehensions replace loops that build up a list, and they do it in one readable line. Do not nest them more than one level deep - past that, a regular loop is clearer.
Functions
Functions are Python’s primary unit of reuse and abstraction. We will go deep on this in the next post; for now, the essentials:
def greet(name, greeting="Hello"):
"""Return a greeting string. The triple-quoted string is a docstring."""
return f"{greeting}, {name}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", "Hi")) # Hi, Bob!
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict:
def summarise(*numbers):
return sum(numbers) / len(numbers)
summarise(10, 20, 30) # 20.0
The Import System
Python’s standard library and third-party packages are accessed via import:
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.141592653589793
from collections import Counter
words = ["apple", "banana", "apple", "cherry"]
print(Counter(words)) # Counter({'apple': 2, 'banana': 1, 'cherry': 1})
Third-party packages are installed via pip install package-name. Serious projects use virtual environments (python -m venv venv) to isolate dependencies per project, avoiding version conflicts between projects.
Gotchas: The Places Python Will Surprise You
Mutable default arguments. Default argument values are evaluated once when the function is defined, not each time it is called. Using a mutable object (list, dict) as a default is almost always a bug:
# WRONG - the same list is shared across all calls
def add_item(item, items=[]):
items.append(item)
return items
add_item(1) # [1]
add_item(2) # [1, 2] - not [2]!
# RIGHT - use None as sentinel, create fresh list inside
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
is vs ==. == compares values. is compares identity (same object in memory). Use == for comparisons; reserve is for checking against None, True, and False:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True - same value
print(a is b) # False - different objects
Integer caching. CPython caches small integers (typically -5 to 256). a = 256; b = 256; a is b gives True. a = 257; b = 257; a is b gives False. This is an implementation detail, not a language guarantee. Never use is for integer comparison.
Floats are not exact. 0.1 + 0.2 gives 0.30000000000000004. This is IEEE 754 floating point, not Python’s fault - but Python will cheerfully show you the full imprecision when you print it. For financial calculations, use the decimal module.
When Not to Use Python
Python is the wrong choice when:
- Raw performance is critical. CPU-intensive loops in Python are 10 - 100x slower than the same code in C, C++, or Rust. Game engines, embedded systems, and real-time signal processing need compiled languages.
- True parallelism on a single machine is needed. The GIL means threads do not help for CPU work.
multiprocessinghelps but adds overhead. - Type safety is a hard requirement at scale. Dynamic typing means large teams can produce large codebases where types are unclear and hard to refactor. Python now has optional type hints and
mypy, which helps significantly, but the ecosystem still lags behind TypeScript or Java in this respect. - Mobile applications. Python does not have a first-class story for iOS or Android development.
None of these are reasons not to learn Python. They are reasons to understand that Python is a tool, and like all tools, it has the shape of the problems it was built to solve.
Where Python Actually Lives
Python’s strength is the glue and the exploration layer. The machine learning models that power modern AI are implemented in C++ and CUDA; Python provides the interface scientists use to compose, train, and evaluate them. Web backends built with Django or FastAPI are Python all the way down, typically making database calls that dominate the runtime anyway. Data pipelines, ETL scripts, automation tools, research code - these are where Python excels because speed of writing and ecosystem access matter more than speed of execution.
The language Guido built in a Christmas holiday, named after a comedy troupe, has ended up powering a remarkable amount of the world’s infrastructure. Understanding its philosophy - readable code, dynamic typing, batteries included, speed of iteration over speed of execution - explains not just the syntax, but why the syntax is the way it is.
| Feature | Python’s choice | Consequence |
|---|---|---|
| Typing | Dynamic | Fast to write, runtime errors possible |
| Execution | Interpreted | Easy REPL, slower than compiled |
| Concurrency | GIL | Bad for CPU parallelism, fine for I/O |
| Ecosystem | PyPI + stdlib | Excellent for science and data, good everywhere |
| Syntax | Indentation-based | Enforced readability, unusual for beginners |
Read Next: