1.1 Language Overview and Execution Model
Covers what Python is, how it actually runs, the feature and limitation lists from the notes, and the two foundational claims the notes open with: Python is dynamically typed, and in Python everything is an object.
1.1.1 What Python Is
Explanation
Python is a general purpose high level programming language.
Two independent claims:
General purpose — Python is not tied to one problem domain. The same language is used for scripting, web backends, automation, data analysis, machine learning, testing, GUIs and glue code. Contrast with domain-specific languages: SQL only queries data, HTML only describes documents, MATLAB targets numerical work.
High level — the language is far from the machine. There are no registers, no pointer
arithmetic, no manual malloc/free, no explicit types. You describe what should happen;
the interpreter decides how.
Comparison — level of abstraction
| Level | Language | What you must manage yourself |
|---|---|---|
| Low | Assembly | Registers, memory addresses, instructions |
| Mid | C | Memory allocation/deallocation, pointers, types |
| High | Python | Almost nothing — memory and types are handled for you |
Why it matters
High level buys developer speed and readability at the cost of execution speed — which is exactly the limitation recorded later in 1.1.6. The two facts are the same trade-off seen from opposite sides.
In practice — Python's real-world footprint. Instagram's backend, Dropbox's file-sync engine, and large parts of YouTube's core services are all built substantially in Python, alongside its heavy use in data science, machine learning research, and scripting at NASA/JPL for mission tooling — a direct payoff of being general-purpose rather than tied to one problem domain.
1.1.2 Dynamically Typed Language
Explanation
Dynamically typed language: given a value, type will be assigned automatically.
You never declare a type. You bind a name to a value, and the type belongs to the value (the object), not to the name. The name is only a label pointing at an object; rebinding it to a different kind of object is legal.
Syntax
x = 10 # Python — no type declaration
int x = 10; // Java — type declared, fixed forever
x = 5
print(type(x)) # <class 'int'>
x = "hi"
print(type(x)) # <class 'str'> -- same name x, rebound to a different type of object
<class 'int'> <class 'str'>
Important behaviour — dynamic typing is not weak typing
This is the single most common confusion around this note. Python is dynamically typed (types are resolved at runtime) and strongly typed (it will not silently convert unrelated types for you).
Comparison — two independent axes
| Language | Typing discipline | Type strength | "2" + 2 gives |
|---|---|---|---|
| Java, C++ | Static (checked before running) | Strong | Compile error |
| Python | Dynamic (checked while running) | Strong | TypeError at runtime |
| JavaScript | Dynamic | Weak | "22" — silently coerced |
Common mistakes
- Saying "the variable is an integer". The object is an integer; the variable is a name.
- Treating dynamic typing as "no types" — Python has types, it just does not require you to write them down.
- Assuming dynamic typing implies automatic conversion. It does not.
In practice — why large Python codebases adopt mypy. Companies running Python at
scale (Dropbox and Instagram are both well-documented examples) layer a static type
checker like mypy/pyright on top of their codebase specifically because dynamic
typing without one gets harder to safely refactor as a codebase grows — the checker
gives back some of the "caught before running" guarantee a statically-typed language
has, without giving up dynamic typing itself.
1.1.3 In Python, Everything Is an Object
Explanation
In Python everything is an object — variable, method, everything.
Every value you can bind to a name is an object, and every object has three things:
an identity (its address, via id()), a type, and a value. Crucially this
includes things that are not objects in most other languages: functions, methods, classes,
modules, and even types themselves.
x = 5
print(id(x), type(x), x) # e.g. 132695655973232 <class 'int'> 5 -- identity, type and value, all at once
10454408 <class 'int'> 5
Functions, classes, modules, built-in types such as int, and even type itself all report as objects — nothing is a special non-object case.
Why it matters
This is not trivia; it is the property that makes large parts of Python possible. Because functions are objects, they can be stored in variables, passed as arguments, given attributes and returned from other functions — which is what enables higher-order functions (6.8.2), closures and decorators.
Edge case / precision
The note says "variable... everything", and the spirit is right, but two refinements are worth writing down now:
- A variable is not itself an object — it is a name in a namespace that refers to an
object. The object is what has identity, type and value. (
id(x)returns the id of the object, not ofx.) - Syntax is not an object. Keywords and operators (
if,while,=,+) are language grammar, not values — you cannot bindifto a name. Everything that is a value is an object; the grammar is not a value.
1.1.4 Interpreted — the Execution Model
Explanation
The note lists interpreted as a feature. What actually happens is a two-stage process, and knowing both stages explains several behaviours later in the notebook.
source.py
│ (1) compile — done automatically by Python
▼
bytecode ────────► cached in __pycache__/source.cpython-3xx.pyc
│ (2) interpret — instruction by instruction
▼
Python Virtual Machine (PVM)
│
▼
operating system / CPU
So Python is compiled — just not to machine code. It compiles to bytecode, a portable intermediate instruction set, which the PVM then executes. This is precisely the mechanism that delivers platform independence (1.1.5): the bytecode is the same everywhere, only the PVM differs per OS.
Important behaviour — the compile step catches errors before anything runs
Because compilation happens first and covers the whole file, a syntax error means no line executes at all. A name error is only found when execution reaches that line.
Comparison — "interpreted" is a property of the implementation, not the language
| Implementation | Written in | How it runs your code |
|---|---|---|
| CPython (the default) | C | Compiles to bytecode, interprets it |
| PyPy | Python (RPython) | Bytecode + JIT compilation to machine code |
| Jython | Java | Compiles to Java bytecode, runs on the JVM |
| IronPython | C# | Runs on .NET |
The language specification says nothing about interpretation. "Python is interpreted" is shorthand for "CPython interprets bytecode".
Common mistakes
- Thinking
.pycfiles make the program run faster. They only skip recompilation at import time; execution speed is unchanged. - Expecting
__pycache__for the script you run directly — only imported modules get cached, not the top-level script.
Demo — watching __pycache__ appear
The diagram above is not just theory — importing a module really does leave a compiled
.pyc file behind. To show this without depending on any file already on disk, the demo
below creates a throwaway module in a temporary directory first.
Note — the tempfile.mkdtemp() + write-a-.py-file step below is just
notebook scaffolding to get a real, importable module on disk; it has nothing to do with
the bytecode-caching behaviour being demonstrated.
import sys, tempfile, os
demo_dir = tempfile.mkdtemp()
with open(os.path.join(demo_dir, "tinymod.py"), "w") as f:
f.write("VALUE = 42\n")
sys.path.insert(0, demo_dir)
import tinymod
print(tinymod.VALUE) # 42
print(sorted(os.listdir(demo_dir))) # ['__pycache__', 'tinymod.py']
print(os.listdir(os.path.join(demo_dir, "__pycache__")))
# ['tinymod.cpython-3XX.pyc'] -- the compiled bytecode, tagged with the interpreter version
42 ['__pycache__', 'tinymod.py'] ['tinymod.cpython-313.pyc']
1.1.5 Features of Python
The full list from the notes, with what each one actually claims:
| # | Feature | Meaning |
|---|---|---|
| 1 | Simple and easy to learn | Small keyword set, English-like syntax, indentation instead of braces, no boilerplate |
| 2 | Freeware and open source | Free to use, source available, permissive PSF licence — usable commercially |
| 3 | High level | No memory management, no pointers, no type declarations (see 1.1.1) |
| 4 | Platform independent | Same source runs on Windows, Linux, macOS — via bytecode + per-OS PVM |
| 5 | Portability | Code migrates from one OS to another without any changes |
| 6 | Both procedure oriented and object oriented | Multi-paradigm — write either style, or mix them |
| 7 | Interpreted | Bytecode executed by the PVM, no separate build step (see 1.1.4) |
| 8 | Extensible | Code from other languages (C/C++, Java) can be integrated into Python |
| 9 | Embedded | Python code can be embedded inside programs written in other languages |
| 10 | Extensive library | "Batteries included" — a large standard library plus a huge third-party ecosystem |
Platform independence and portability (features 4 and 5)
The notes list these separately, and there is a real distinction:
- Platform independence is a property of the implementation — the bytecode is
OS-neutral, so the same
.pyfile runs anywhere a PVM exists. - Portability is the practical consequence stated in the notes: migrating code from one OS to another without any changes.
The interpreter absorbs the OS differences — path separators, line endings, system calls — so your source does not have to.
Edge case: portability is "without changes" only while you stay inside the
abstractions. Hard-coding "C:\\data\\file.txt", shelling out to dir or ls, or relying
on a C extension that has no build for the target OS all break it. The language is portable;
a given program is portable only if written that way.
Extensible and embedded (features 8 and 9)
These two are constantly swapped in exam answers. They are opposite directions:
Extensible : C / C++ / Java code ──► used from inside Python
Embedded : Python code ──► used from inside a C / C++ / Java program
Extensibility is not theoretical — a large part of the standard library itself is implemented in C, with the Python layer only providing the interface around it.
This is also why the performance limitation in 1.1.6 is survivable: heavy work is pushed down into C extensions (NumPy, pandas) while the Python layer just orchestrates.
Both procedure oriented and object oriented (feature 6)
Python supports procedural, object-oriented and even functional style for the same task — none is "the" Python way.
In practice — this is why NumPy/pandas are fast despite being "Python." Both libraries implement their actual number-crunching in C (extensibility, in action), with the Python layer only orchestrating calls into it — which is exactly why vectorized NumPy code runs orders of magnitude faster than an equivalent hand-written Python loop over the same data.
Extensive library (feature 10)
The standard library ships with the interpreter — no installation needed for file handling, regex, JSON, databases, threading, testing, math, dates, networking and more. Nearly every chapter in this notebook is backed by a stdlib module.
1.1.6 Limitations of Python
From the notes:
1 Interpreter performance is always low
Why — every operation pays overhead that a compiled language does not:
- bytecode is dispatched instruction by instruction at runtime,
- types are unknown until runtime, so every operation must be looked up dynamically
(
a + bcannot be reduced to one CPU instruction — Python must ask whatais first), - every value is a heap object with a header, so even small integers cost memory,
- in CPython, the GIL prevents threads from running Python bytecode in parallel (17.5).
Going deeper — CPython also offers an optional free-threaded build that removes the GIL entirely, distinct from the standard build most people actually run. "The GIL always applies" describes that standard build specifically, not an absolute rule for every CPython build. See 17.5 for the timed proof of the GIL's CPU-bound-vs-I/O-bound effect, and for how that proof changes on the free-threaded build.
Against a compiled language like C, pure Python loops doing equivalent arithmetic are typically one to two orders of magnitude slower.
Qualifying "always" — worth writing in the margin, because interviewers push on it:
- CPU-heavy work is normally delegated to C extensions (NumPy, pandas), so the Python layer only orchestrates and the penalty largely disappears.
- Alternative runtimes change the picture — PyPy's JIT, or Cython / C extensions for hot paths.
- For I/O-bound work (network, disk, database) the bottleneck is waiting, not the interpreter, and Python is no slower than anything else.
So: the interpreter is slower, but "always low performance" describes the interpreter, not necessarily the program.
2 Mobile app development
Python has no first-class position on mobile. There is no standard Python UI toolkit for iOS or Android, app-store packaging is awkward, and runtime size and battery cost work against it. Frameworks exist (Kivy, BeeWare) but they are niche compared with Swift/Kotlin or cross-platform frameworks.
This is softening rather than fixed: CPython now includes officially supported (though lower-tier) iOS and Android build targets, so the gap is narrowing — but as a practical recommendation the limitation still holds.
Further limitations to be added if the notes list more (e.g. GIL, memory footprint, runtime-only error detection).
In practice — offloading CPU-heavy work out of the request cycle. A web backend that needs to do something genuinely CPU-intensive (resizing an image, generating a PDF, running a scoring model) typically hands that work to a background worker system like Celery instead of doing it inline while a user waits — sidestepping this exact interpreter-performance limitation rather than fighting it.
1.1.7 Distinctions Worth Pinning Down
Pairs from this section that are routinely confused — useful as a self-test:
| Often confused | The actual difference |
|---|---|
| Dynamically typed vs weakly typed | When types are checked vs whether types are enforced. Python is dynamic and strong. |
| Platform independent vs portable | A property of the implementation (OS-neutral bytecode) vs the practical result (no code changes when migrating). |
| Extensible vs embedded | Other languages used inside Python vs Python used inside other languages. |
| Interpreted vs compiled | Python does both — compiles to bytecode, then interprets it. "Interpreted" describes CPython, not the language. |
| Variable vs object | The name vs the thing it points to. Only the object has a type. |
1.2 Identifiers and Reserved Keywords in Python
Identifiers
An identifier is the name given to a variable, function, class, module or any other object in Python. Rules for a valid identifier:
- May contain letters (
a-z,A-Z), digits (0-9) and the underscore_. - Must not start with a digit.
- Cannot be a reserved keyword.
- Case-sensitive (
ageandAgeare different identifiers). - No spaces or special characters (
@,$,%,-, etc.) are allowed.
Reserved Keywords
Keywords are words that are reserved by Python and cannot be used as identifiers. Python 3.x has 35 reserved keywords:
False None True and as assert async await
break class continue def del elif else except
finally for from global if import in is
lambda nonlocal not or pass raise return try
while with yield
import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class',
# 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global',
# 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise',
# 'return', 'try', 'while', 'with', 'yield']
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
A keyword can never be used as an identifier — trying to bind a value to one is a
SyntaxError, caught here at compile time before anything runs (see 1.1.4):
Going deeper — keyword.softkwlist holds Python's soft keywords — names like
match, case, _, and type that are only special in the specific
syntactic position they were introduced for (match/case statements, wildcard patterns,
type aliases). Everywhere else they remain perfectly ordinary identifiers, unlike the
words in kwlist above.
try:
compile("class = 5", "<test>", "exec")
except SyntaxError as e:
print(e) # invalid syntax (<test>, line 1) -- 'class' is a keyword, not a valid identifier
invalid syntax (<test>, line 1)
1.3 Syntax, Statements and Code Structure
Statements — A statement is a complete instruction Python executes (an assignment, a
function call, a return, an import, etc.). Normally one statement per line; ; lets you
chain multiple statements on one line (discouraged), and a line ending in \, or an
unclosed bracket, lets a single statement continue across multiple lines.
Indentation defines blocks — Python uses indentation, not braces {}, to mark the body
of a function, loop, conditional, class, etc. Indentation is not a style choice —
inconsistent indentation raises an IndentationError. Convention is 4 spaces per level;
tabs and spaces must not be mixed.
The colon : introduces a block header — if, for, while, def, class, try,
with all end their header line with :, followed by an indented block.
Case sensitivity — Python is case-sensitive: Name and name are different
identifiers, and keywords must be written exactly as reserved (lowercase, except True,
False, None).
Comments — anything after a # on a line is ignored entirely by Python; it exists
purely for human readers. Comments are used constantly from chapter 2 onward (to annotate
expected output, for instance) without ever being formally introduced until now.
x = 5 # this is a comment, ignored by Python
print(x) # 5
5
Indentation in practice — the block below (the print("adult") line) only runs
because it is indented under the if; the line after it is not indented, so it is
outside the if block and always runs regardless of the condition.
Going deeper — the walrus operator := assigns a value
and produces it as an expression result in one step (e.g. inside an if condition).
It is covered in full in chapter 3.
age = 20
if age >= 18:
print("adult") # inside the block - runs because indented under if
print("done") # outside the block - always runs, indentation ends the if
adult done
Naming conventions (PEP 8) — the identifier rules above say what is legal; PEP 8
(the standard Python style guide) additionally recommends what is conventional:
snake_case (lowercase words separated by underscores) for variables and functions,
PascalCase (each word capitalized, no separators) for classes, and UPPER_CASE for
constants. None of this is enforced by the interpreter — code that ignores it still runs
— but following it is what makes Python code immediately readable to other Python
developers.
In practice — enforced automatically, not by memory. Real teams don't rely on
everyone remembering PEP 8 — a formatter (black) and a linter (ruff/flake8) run
in CI and/or as a pre-commit hook, automatically rejecting or auto-fixing code that
doesn't conform, so style consistency doesn't depend on manual code review catching it.
1.4 Basic Input and Output
Output — print() — Prints any number of values to standard output, converting each to
its string form automatically. sep (default " ") controls the separator between values;
end (default "\n") controls what is written after the last one.
Input — input([prompt]) — Reads a line of text typed by the user and always returns it
as a str, regardless of what it looks like — explicit conversion (int(), float(), ...)
is required to use it as another type. The optional prompt argument is displayed before
the program waits for input.
Formatted output — f-strings (f"...{value}...") are the standard way to embed values
inside printed text without manual string concatenation.
print("a", "b", sep="-", end="!\n") # a-b!
a-b!
Note — the line below monkeypatches input() only so this cell can run
non-interactively in the notebook; real usage is just age = int(input("Enter your age: ")).
import builtins
builtins.input = lambda prompt="": "42" # non-interactive stand-in for this demo only
age = int(input("Enter your age: "))
print(age) # 42
42