Learn Python
Python is a dynamically typed, garbage-collected, multi-paradigm language created by Guido van Rossum and first released in 1991. It prizes readability above almost everything else: significant indentation instead of braces, a small set of orthogonal constructs, and a culture summarized in the Zen of Python ('readable counts'). The current feature release is Python 3.14, shipped on 7 October 2025, which made the free-threaded (no-GIL) build officially supported and added template strings, deferred annotations, and a tail-call interpreter. This track teaches Python from the ground up for the Eulingua reader who already knows another language such as Guji or Go: installing the interpreter and setting up tooling, the syntax and dynamic type system, functions and the many ways to pass arguments, the four core collection types, idiomatic error handling with exceptions and exception groups, concurrency with asyncio and threads, and the packaging ecosystem around pip, venv, and uv. Every example here was run against a real CPython interpreter, and every lesson links to the official documentation at docs.python.org.
Setup and the Python Toolchain
Install Python, run your first program, and set up virtual environments.
Setup and the Python Toolchain
Python is an interpreted language: you run .py source files directly, no separate compile step. The reference implementation is CPython, and the command that runs it is python3 (or just python on many systems). This lesson takes you from zero to a running program with a clean, isolated environment.
Installing Python
Download an installer for your platform from the official site, or use a package manager (brew install python, apt install python3, etc.). After installing, confirm the version:
$ python3 --version
Python 3.14.0
As of 7 October 2025 the current feature release is Python 3.14. Python ships a new feature release every October and supports each one for five years, so 3.14 receives bug fixes and security patches well into the 2030s. The previous series, 3.13, still gets maintenance releases (3.13.14 landed on 10 June 2026).
Your first program
Python needs no main function and no boilerplate. Create a file named hello.py:
print("Hello, Python!")
Run it:
$ python3 hello.py
Hello, Python!
You can also start an interactive REPL by running python3 with no arguments. The 3.14 REPL has color, multi-line editing, and syntax highlighting built in - excellent for experiments:
$ python3
>>> 2 ** 10
1024
>>> "ha" * 3
'hahaha'
The __main__ guard is a near-universal idiom. It lets a file act both as a runnable script and an importable module:
def main():
print("Hello, Python!")
if __name__ == "__main__":
main()
When you run the file directly, __name__ equals "__main__" and main() is called; when another module imports it, the guard is false and nothing runs.
Virtual environments: isolate every project
Never install project dependencies into the system Python. Instead create a virtual environment - a self-contained directory with its own copy of the interpreter and its own site-packages. The standard tool is venv, which ships with Python:
$ python3 -m venv .venv
$ source .venv/bin/activate # Windows: .venv\Scripts\activate
(.venv) $ python -m pip install requests
(.venv) $ deactivate
While the environment is active, python and pip refer to the ones inside .venv. Deactivating restores your shell. Add .venv/ to .gitignore; it is rebuildable from your dependency list.
uv: the fast modern alternative
In 2026 many teams reach for uv, a Rust-based tool from Astral that replaces pip, venv, pip-tools, and pyenv with one binary. It is dramatically faster - creating an environment that python -m venv takes about a second to build finishes in roughly ten milliseconds. A typical uv workflow:
$ uv init myapp && cd myapp
$ uv add requests
$ uv run main.py
uv reads and writes the standard pyproject.toml, so it interoperates with the rest of the ecosystem. We cover packaging in depth in the final lesson; for now, python -m venv is always available and is the right thing to learn first.
The commands you will use daily
| Command | What it does |
|---|---|
python3 script.py |
Run a script |
python3 -m module |
Run a module as a script (e.g. -m venv, -m pip) |
python3 -c 'code' |
Run a one-liner |
python -m pip install pkg |
Install a package into the active environment |
python -m pytest |
Run tests (after installing pytest) |
Note the python -m pip form rather than a bare pip - it guarantees you are using the pip that belongs to the interpreter you think you are using.
Where to go next
The official tutorial is thorough and beginner-friendly, and the setup guide covers installation on every platform.
- The Python Tutorial: https://docs.python.org/3/tutorial/index.html
- Installing Python and using venv: https://docs.python.org/3/library/venv.html
With the interpreter working and an environment activated, you are ready to learn the language itself, starting with its syntax and types.
Syntax, Variables, and the Type System
Indentation, dynamic typing, the core scalar types, and control flow.
Syntax, Variables, and the Type System
Python's syntax is famous for using indentation to delimit blocks instead of braces. This is not optional style - it is the grammar. A consistent four-space indent groups statements; a misaligned line is a syntax error. The result is that all idiomatic Python looks structurally similar, much as gofmt enforces in Go.
Variables are names bound to objects
Python is dynamically typed: a variable is just a name pointing at an object, and you never declare a type. Reassign freely:
name = "Ada"
count = 0
pi = 3.14159
name = 42 # legal: name now points at an int
print(type(name)) # <class 'int'>
Everything is an object, including functions and classes. There is no separate notion of a primitive value.
Type hints: optional, static, ignored at runtime
Since Python 3.5 you can annotate names and signatures. These type hints are checked by external tools like mypy or pyright but have no effect at runtime - Python never enforces them:
name: str = "Ada"
age: int = 37
def area(w: float, h: float) -> float:
return w * h
Hints are how modern Python codebases get IDE autocompletion and catch type errors before running. In Python 3.14 (PEP 649/749) annotations are evaluated lazily, which makes forward references work without quoting.
The core scalar types
int- arbitrary precision;2 ** 200is exact, no overflow.float- 64-bit IEEE 754 double.bool-True/False(a subtype ofint).str- immutable Unicode text.bytes- immutable raw bytes.None- the unique null/absent value.
big = 2 ** 200
print(big) # 1606938044258990275541962092341162602522202993782792835301376
print(10 / 3) # 3.3333333333333335 (true division, always float)
print(10 // 3) # 3 (floor division)
print(10 % 3) # 1 (modulo)
Strings and f-strings
Strings are immutable and Unicode. The idiomatic way to build them is the f-string, a literal prefixed with f that interpolates expressions inside {}:
name, age = "Ada", 37
print(f"{name} is {age}") # Ada is 37
print(f"pi is about {3.14159:.2f}") # pi is about 3.14
print(f"{age = }") # age = 37 (the = shows the expression too)
Python 3.14 also adds t-strings (PEP 750): a t"..." literal returns a Template object that keeps the literal and interpolated parts separate, which libraries use for safe HTML/SQL building. f-strings remain what you reach for in everyday code.
Truthiness
Every object is either truthy or falsy. Empty containers ("", [], {}, ()), 0, and None are falsy; almost everything else is truthy. This drives idioms like if items: to mean "if the list is non-empty."
Control flow
if / elif / else, for, and while cover the basics. A for loop iterates over any iterable, not an index:
for ch in "abc":
print(ch)
for i, val in enumerate(["a", "b", "c"]):
print(i, val)
while count < 3:
count += 1
The walrus operator := assigns inside an expression, handy in conditions:
if (n := len(name)) > 2:
print(f"name has {n} characters")
Pattern matching
Since Python 3.10, match / case does structural pattern matching - far more than a C switch. It can destructure sequences and capture parts:
def describe(cmd: str) -> str:
match cmd.split():
case ["go", direction]:
return f"moving {direction}"
case ["stop"]:
return "stopping"
case [action, *rest]:
return f"{action} with {rest}"
case _:
return "unknown"
print(describe("go north")) # moving north
print(describe("jump high")) # jump with ['high']
The case _ is the wildcard default. Patterns can also match by class and extract attributes.
Reference
The official tutorial chapters on control flow and on the data model explain the why behind these rules.
- More control flow tools: https://docs.python.org/3/tutorial/controlflow.html
- The Python language reference (data model): https://docs.python.org/3/reference/datamodel.html
Next we package behavior into functions and learn Python's unusually flexible argument passing.
Functions, Arguments, and Closures
Defaults, *args/**kwargs, keyword-only params, lambdas, and decorators.
Functions, Arguments, and Closures
Functions are first-class objects in Python: you can assign them to variables, pass them as arguments, return them, and attach attributes. Python's calling convention is also one of the richest of any mainstream language - defaults, keyword arguments, variadic collection, and keyword-only parameters all compose.
Defining and calling
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Ada") # 'Hello, Ada!'
greet("Ada", "Hi") # 'Hi, Ada!'
greet(name="Ada", greeting="Hey") # by keyword, any order
Parameters with a = have default values. Callers may pass arguments positionally or by keyword, which makes call sites self-documenting.
The mutable default trap
Default values are evaluated once, when the function is defined - not on each call. A mutable default like [] is therefore shared across calls and is a classic bug:
def bad(item, bucket=[]): # DON'T: shared list
bucket.append(item)
return bucket
def good(item, bucket=None): # DO: sentinel + fresh list
if bucket is None:
bucket = []
bucket.append(item)
return bucket
*args and **kwargs
A *args parameter collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict:
def report(*nums, **labels):
return sum(nums), labels
report(1, 2, 3, scale="m") # (6, {'scale': 'm'})
The same * and ** work at the call site to unpack a sequence or mapping into arguments:
values = [1, 2, 3]
report(*values) # spreads into nums
opts = {"scale": "km"}
report(1, **opts) # spreads into labels
Keyword-only and positional-only parameters
A bare * in the signature forces every parameter after it to be passed by keyword - useful for flags that should never be a mystery positional True:
def connect(host, *, timeout=30, secure=True):
...
connect("db", timeout=5) # ok
# connect("db", 5) # TypeError: timeout is keyword-only
Symmetrically, parameters before a / are positional-only.
Lambdas and closures
A lambda is a small anonymous function limited to a single expression. Functions also form closures, capturing variables from the enclosing scope:
def adder(n):
return lambda x: x + n
add5 = adder(5)
print(add5(10)) # 15
The returned lambda remembers n. Lambdas shine as throwaway callbacks, e.g. sorted(words, key=lambda w: len(w)).
Decorators
Because functions are values, you can write a function that wraps another to add behavior. The @decorator syntax applies it:
import functools, time
def timed(fn):
@functools.wraps(fn) # preserve name/docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.perf_counter()-start:.4f}s")
return result
return wrapper
@timed
def work(n):
return sum(range(n))
work(1_000_000)
@timed is exactly work = timed(work). Decorators power much of the framework ecosystem - Flask routes, pytest fixtures, @dataclass, @property, and more.
Generators
A function that uses yield is a generator: it produces values lazily, one at a time, holding no more than the current one in memory:
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
import itertools
print(list(itertools.islice(fib(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13]
Generators let you iterate over infinite or very large sequences with constant memory.
Reference
- Defining functions (tutorial): https://docs.python.org/3/tutorial/controlflow.html#defining-functions
- functools (decorators helpers): https://docs.python.org/3/library/functools.html
Next: the four collection types you will reach for in nearly every program.
Collections: Lists, Tuples, Dicts, and Sets
The four built-in containers and the comprehensions that build them.
Collections: Lists, Tuples, Dicts, and Sets
Python gives you four general-purpose built-in containers. Knowing which to reach for - and the comprehension syntax that builds each one - is most of day-to-day Python.
Lists: ordered, mutable
A list is a growable, ordered sequence written with square brackets:
nums = [3, 1, 2]
nums.append(4) # [3, 1, 2, 4]
nums.sort() # [1, 2, 3, 4] (in place)
nums[0] = 99 # mutable
print(nums[-1]) # 4 (negative indexes from the end)
print(nums[1:3]) # [2, 3] (slicing)
Slicing seq[start:stop:step] works on any sequence and never raises for out-of-range bounds. nums[::-1] reverses a list.
Tuples: ordered, immutable
A tuple is a fixed sequence written with parentheses (or just commas). Because it cannot change, it can be a dict key and signals "these belong together":
point = (3, 4)
x, y = point # unpacking
print(x, y) # 3 4
first, *rest = [1, 2, 3, 4]
print(first, rest) # 1 [2, 3, 4]
Multiple return values are really just a tuple: return a, b then lo, hi = f().
Dicts: key/value mappings
A dict maps keys to values and, since Python 3.7, preserves insertion order. Lookups, inserts, and deletes are average O(1):
ages = {"Ada": 37, "Bob": 24}
ages["Cleo"] = 50
print(ages["Ada"]) # 37
print(ages.get("Dan", 0)) # 0 (default avoids KeyError)
for name, age in ages.items():
print(name, age)
Use .get(key, default) or in to avoid KeyError. Merge dicts with the | operator: defaults | overrides.
Sets: unordered unique elements
A set holds distinct, hashable values and supports fast membership tests and mathematical set algebra:
a = {1, 2, 3}
b = {2, 3, 4}
print(a & b) # {2, 3} intersection
print(a | b) # {1, 2, 3, 4} union
print(a - b) # {1} difference
print(2 in a) # True
Deduplicate a list with list(set(items)) (order is not preserved).
Comprehensions
The single most Pythonic feature is the comprehension: a compact expression that builds a collection from an iterable, optionally filtering. There is one for each container:
squares = [n * n for n in range(5)] # list -> [0, 1, 4, 9, 16]
evens = [n for n in range(10) if n % 2 == 0] # with a filter
lengths = {w: len(w) for w in ["a", "bb"]} # dict -> {'a': 1, 'bb': 2}
letters = {c for c in "banana"} # set -> {'b', 'a', 'n'}
Wrapping the same syntax in parentheses gives a lazy generator expression, which streams values without building the whole collection in memory:
total = sum(n * n for n in range(1_000_000)) # constant memory
Prefer a comprehension over a manual for loop with .append() when you are transforming or filtering - it is faster and reads as a single intention. Do not abuse them: if logic grows past a line or two, a plain loop is clearer.
Choosing a container
| Need | Use |
|---|---|
| Ordered, will change | list |
| Fixed record / dict key | tuple |
| Lookup by key | dict |
| Uniqueness / set math | set |
For more, the collections module adds Counter, defaultdict, deque, and namedtuple, and dataclasses gives you typed record classes with almost no boilerplate.
Reference
- Data structures (tutorial): https://docs.python.org/3/tutorial/datastructures.html
- The collections module: https://docs.python.org/3/library/collections.html
Next: what happens when something goes wrong - Python's exception model.
Error Handling with Exceptions
try/except/else/finally, raising, custom exceptions, and exception groups.
Error Handling with Exceptions
Python reports errors by raising exceptions and handling them with try / except. Unlike Go's explicit error returns, Python's control flow jumps to the nearest matching handler. The cultural rule is EAFP - "it's Easier to Ask Forgiveness than Permission" - meaning you try the operation and catch the failure rather than checking preconditions up front.
try / except / else / finally
def parse(s):
try:
n = int(s)
except ValueError as e:
return f"bad input: {e}"
else:
return f"parsed {n}" # runs only if no exception
finally:
print("done") # always runs, error or not
parse("42") # prints 'done', returns 'parsed 42'
parse("abc") # prints 'done', returns 'bad input: ...'
exceptcatches a specific exception type. Theas ebinds the exception object.elseruns only when thetryblock did not raise.finallyalways runs - for cleanup like closing files, even if an exception propagates.
Catch the narrowest exception you can handle. A bare except: or except Exception: that swallows everything hides bugs.
Catching multiple types
List several types in a tuple. In Python 3.14 (PEP 758) you may drop the parentheses when there is no as clause:
try:
risky()
except (ValueError, KeyError) as e: # works on every version
handle(e)
except ValueError, KeyError: # 3.14+: parentheses optional
handle_them()
Raising and re-raising
Use raise to signal an error, and a bare raise inside an except block to re-throw the current one after logging:
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"cannot withdraw {amount} from {balance}")
return balance - amount
When you catch one error and raise another, chain them with from so the traceback shows both causes:
try:
config = load()
except FileNotFoundError as e:
raise RuntimeError("startup failed") from e
Custom exceptions
Define your own by subclassing Exception. A small hierarchy lets callers catch your whole library's errors with one except:
class AppError(Exception):
"""Base class for this application's errors."""
class NotFoundError(AppError):
pass
try:
raise NotFoundError("user 7")
except AppError as e: # catches NotFoundError too
print("handled:", e)
Context managers replace finally
The with statement guarantees cleanup without a manual finally. The most common use is files, which are closed automatically when the block exits, even on error:
with open("data.txt", encoding="utf-8") as f:
text = f.read()
# f is closed here, no matter what
You can write your own context managers as a class with __enter__/__exit__, or far more easily with @contextlib.contextmanager around a generator.
Exception groups (3.11+)
Sometimes several errors happen at once - think a batch of parallel tasks where three fail. An ExceptionGroup bundles them, and the except* syntax handles each kind separately, even when both occur:
try:
raise ExceptionGroup(
"batch failed",
[ValueError("v"), TypeError("t")],
)
except* ValueError as eg:
print("values:", [str(e) for e in eg.exceptions])
except* TypeError as eg:
print("types:", [str(e) for e in eg.exceptions])
Both except* blocks can run. This is the foundation of robust error handling in asyncio.TaskGroup, covered next.
Reference
- Errors and exceptions (tutorial): https://docs.python.org/3/tutorial/errors.html
- Built-in exceptions and exception groups: https://docs.python.org/3/library/exceptions.html
Next: doing many things at once with asyncio, threads, and the free-threaded build.
Concurrency: asyncio, Threads, and Free Threading
async/await, TaskGroup, the GIL, threads vs processes, and no-GIL Python.
Concurrency: asyncio, Threads, and Free Threading
Python offers three distinct concurrency models, and choosing the right one depends entirely on whether your work is I/O-bound (waiting on the network or disk) or CPU-bound (crunching numbers). This lesson covers all three plus the headline change of Python 3.14: officially supported free-threaded builds.
The GIL, briefly
Historically CPython has a Global Interpreter Lock (GIL): only one thread executes Python bytecode at a time. This makes single-threaded code fast and C extensions simple, but it means threads cannot run pure-Python CPU work in parallel. The GIL shapes every concurrency decision below.
asyncio: cooperative concurrency for I/O
For I/O-bound work - web servers, scrapers, API clients - asyncio lets a single thread juggle thousands of tasks. You write async def coroutines and await points where the task yields control while waiting:
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay) # yields to other tasks while waiting
return f"{name} done"
async def main():
results = await asyncio.gather(
fetch("a", 0.1),
fetch("b", 0.1),
fetch("c", 0.1),
)
print(results) # all three finish in ~0.1s, not 0.3s
asyncio.run(main())
asyncio.gather runs the coroutines concurrently. They overlap their waiting, so three 0.1-second sleeps complete in about 0.1 seconds total. Nothing runs truly in parallel - but while one task waits on I/O, others make progress.
TaskGroup: structured concurrency (3.11+)
The modern way to launch and await a set of tasks is asyncio.TaskGroup. It waits for every task in its block and, if any fail, cancels the rest and raises an ExceptionGroup (from the previous lesson):
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch("x", 0.05))
t2 = tg.create_task(fetch("y", 0.05))
print(t1.result(), t2.result()) # both guaranteed complete here
This "structured concurrency" guarantees no task is left dangling when the block exits.
Threads: for blocking I/O and C extensions
Threads suit blocking I/O in libraries that are not async-aware. The concurrent.futures.ThreadPoolExecutor is the friendly high-level API:
from concurrent.futures import ThreadPoolExecutor
def download(url):
... # a blocking call
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(download, urls))
Because of the GIL, threads do not speed up pure-Python CPU work in the classic build - they overlap only when blocked on I/O or inside C code that releases the GIL (as requests, numpy, and file I/O do).
Processes: for CPU-bound work
To use multiple cores for CPU-bound Python, use ProcessPoolExecutor. Each process has its own interpreter and its own GIL, so they run in genuine parallel - at the cost of pickling arguments and results across process boundaries:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
results = list(pool.map(heavy_compute, chunks))
Free-threaded Python (PEP 703 / PEP 779)
Python 3.14, released 7 October 2025, makes the free-threaded build officially supported (PEP 779). This optional build removes the GIL entirely, replacing it with per-object locking and biased reference counting, so threads can run pure-Python CPU work on multiple cores at once - no separate processes needed.
The trade-offs as of 3.14: single-threaded code runs roughly 5–10% slower (down from 20–40% in the 3.13 preview), and C extensions must be recompiled against the new ABI. You opt in by installing the t variant and the interpreter reports it:
$ python3.14t -c "import sys; print(sys._is_gil_enabled())"
False
This is a multi-year transition; the GIL build remains the default. Python 3.14 also adds concurrent.interpreters (PEP 734), exposing multiple isolated interpreters in one process as another parallelism option.
Choosing a model
| Workload | Reach for |
|---|---|
| Many network/disk calls | asyncio |
| Blocking I/O in non-async libs | ThreadPoolExecutor |
| CPU-bound, classic build | ProcessPoolExecutor |
| CPU-bound, parallel threads | free-threaded build (3.14+) |
Reference
- asyncio documentation: https://docs.python.org/3/library/asyncio.html
- What's New in Python 3.14 (free threading): https://docs.python.org/3/whatsnew/3.14.html
Finally, let us look at how Python projects manage dependencies and ship to the world.
The Ecosystem: pip, pyproject, and PyPI
Dependencies, pyproject.toml, building and publishing, and the wider toolchain.
The Ecosystem: pip, pyproject, and PyPI
Python's reach comes from its enormous third-party ecosystem. The Python Package Index (PyPI) hosts hundreds of thousands of libraries, and pip installs them. This lesson covers how to manage dependencies, structure a project with pyproject.toml, and publish your own package - plus the tooling that keeps a codebase healthy.
Installing packages with pip
With a virtual environment active (lesson one), install from PyPI:
(.venv) $ python -m pip install requests rich
(.venv) $ python -m pip list
Pin and record your dependencies so others can reproduce the environment. The traditional file is requirements.txt:
(.venv) $ python -m pip freeze > requirements.txt
(.venv) $ python -m pip install -r requirements.txt # on another machine
Always pin versions for applications; libraries usually specify looser ranges.
pyproject.toml: the standard project file
Modern Python projects are configured by a single pyproject.toml (standardized in PEP 518/621). It declares your package's metadata, dependencies, and build system in one place:
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.32",
"rich>=13.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
The [build-system] table names the build backend - hatchling, setuptools, flit, or pdm - that turns your source into a distributable artifact. Tools like pytest, ruff, and mypy also read their configuration from this same file under [tool.*] tables.
uv: the fast all-in-one workflow
The biggest tooling shift of recent years is uv, the Rust-based manager from Astral. It manages environments, resolves and locks dependencies into uv.lock, and runs your code, all reading the standard pyproject.toml:
$ uv init myapp && cd myapp
$ uv add requests # adds to pyproject.toml and the lockfile
$ uv run python app.py # runs in the managed environment
$ uv sync # recreate the env from the lockfile
uv is a drop-in replacement for pip, pip-tools, virtualenv, and pyenv combined, and is typically several times faster. The official Packaging User Guide now lists it among recommended tools, alongside the still-perfectly-valid pip plus venv. pip itself is not going away.
Building and publishing to PyPI
To share a library, build the distribution artifacts - a source distribution (sdist) and a wheel - then upload them. With the standard tools:
$ python -m build # writes dist/*.whl and dist/*.tar.gz
$ python -m twine upload dist/* # publish to PyPI
With uv the same two steps are uv build and uv publish. PyPI now requires trusted publishing or API tokens rather than passwords, and publishing from CI via OpenID Connect is the recommended path for projects.
The wider toolchain
A healthy Python project leans on a few standard tools, all configurable from pyproject.toml:
| Tool | Job |
|---|---|
ruff |
Extremely fast linter and formatter (replaces flake8, isort, black) |
mypy / pyright |
Static type checking of your type hints |
pytest |
The de-facto testing framework |
tox / nox |
Run tests across multiple Python versions |
A typical check sequence in CI looks like:
$ ruff format --check .
$ ruff check .
$ mypy src
$ pytest
The standard library is huge
Before reaching for PyPI, remember Python ships "batteries included": json, pathlib, datetime, subprocess, sqlite3, http.server, dataclasses, itertools, re, and dozens more are always available. Many tasks need no dependency at all.
from pathlib import Path
import json
data = json.loads(Path("config.json").read_text())
Reference
- Installing packages (Python Packaging User Guide): https://packaging.python.org/en/latest/tutorials/installing-packages/
- Tool recommendations: https://packaging.python.org/en/latest/guides/tool-recommendations/
That completes the track. You can now set up Python, write idiomatic code across its type and collection systems, handle errors, run concurrent work, and ship a package - the same ground the Eulingua reader covers in tracks like Guji and Go. Keep the official docs at docs.python.org close; they are excellent.