Home› Python› Complete Guide
Complete Guide

Complete Python Tutorial

Python is the go-to language for data science, automation, web development and AI. Whether you're a complete beginner or brushing up for interviews, this guide covers every one of our 171 Python tutorials in one place.

Learning Roadmap
Beginner → Understand Python syntax, data types and control flow
Intermediate → Work with functions, OOP, file I/O and popular libraries
Advanced → Build real projects with Django, data science tools and async Python
171
Topics
55
Beginner
92
Intermediate
24
Advanced
Jump to section
Python Basics (17)Control Flow (7)Data Structures (12)Functions (11)OOP in Python (9)Exception Handling (5)File Handling (6)Python Libraries (57)Advanced Python (17)Packaging (3)Tooling (1)Libraries (3)Basics (2)Errors (18)Web (3)

Python's design goal was that reading code should be easy, and it succeeded so thoroughly that it is possible to be productive for years without learning what the interpreter is actually doing. That works until it does not — until a default argument accumulates state between calls, a closure in a loop captures the wrong thing, or a threaded program refuses to use more than one core.

Every one of those is the same phenomenon: Python's syntax hides a mechanism, and the mechanism eventually asserts itself. The topics that reward real study are exactly the ones where the gap is widest — the data model, name binding and scope, the iterator protocol, and the concurrency story.

Names are bindings, not boxes

In Python a variable is a name bound to an object, not a container holding a value. Assignment rebinds the name; it never copies. That one sentence explains the mutable-default-argument surprise, why passing a list to a function lets the function modify your list, why a = b on a list gives you two names for one object, and why is and == can disagree.

The practical consequence worth internalising: ask whether an object is mutable before assuming an operation is safe. Mutable default arguments and shared nested structures after a shallow copy are the same bug seen from two angles.

python
def add(item, bucket=[]):        # evaluated ONCE, at definition
    bucket.append(item)
    return bucket

add(1); add(2)                   # [1, 2] - one list shared by every call

def add(item, bucket=None):      # the correct idiom
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket

import copy
a = {"tags": ["x"]}
b = copy.copy(a)                 # shallow: the inner list is SHARED
b["tags"].append("y")
a["tags"]                        # ['x', 'y'] - surprised?
c = copy.deepcopy(a)             # independent all the way down

Generators change what is possible, not just what is tidy

A list comprehension builds the whole result in memory. A generator produces one item at a time and holds nothing else. On small data the difference is style; on a ten-gigabyte log file it is the difference between working and a memory error.

The broader idea is the iterator protocol, and once it is clear, a lot of Python stops looking like magic: for loops, unpacking, zip, itertools and file objects are all the same interface. Composing generators gives you a streaming pipeline with constant memory and no framework.

python
# Constant memory regardless of file size - each stage pulls one line
def lines(path):
    with open(path) as f:
        yield from f

def parsed(rows):
    for r in rows:
        if r.strip():
            yield json.loads(r)

def errors(records):
    for rec in records:
        if rec.get("level") == "ERROR":
            yield rec

for rec in errors(parsed(lines("app.log"))):
    handle(rec)

# Nothing is read until the for loop asks for it, and only one
# line exists in memory at a time.
In practiceGenerators are single-use. Iterating one a second time yields nothing, silently — no error, just an empty loop. If you need the data twice, materialise it into a list and accept the memory, or build the generator again.

Concurrency: classify the work before choosing a tool

The GIL means one thread executes Python bytecode at a time. That makes threads useless for CPU-bound work and perfectly fine for I/O-bound work, because the GIL is released while waiting on I/O. Get the classification right and the tool follows.

WorkloadUseWhy
Many network calls, async libraries availableasyncioThousands of outstanding operations on one thread, no context-switch cost
Network or disk work, blocking librariesThreadPoolExecutorThe GIL is released during I/O, so threads genuinely overlap
CPU-bound PythonProcessPoolExecutorSeparate interpreters, separate GILs — real parallelism, at the cost of pickling data between them
CPU-bound numerical workNumPy, Polars, or a native extensionThe heavy loop runs in C with the GIL released; usually faster than multiprocessing and far simpler

Two failure modes are worth naming. A blocking call inside an async function stalls the entire event loop, not just that coroutine — wrap it in run_in_executor. And multiprocessing must pickle everything it sends, so passing large objects to workers can cost more than the work saves.

Packaging and environments, where most lost hours go

More Python time is lost to environment problems than to language problems. The rules that prevent nearly all of them: one virtual environment per project, never install into the system interpreter, pin exact versions in a lock file while keeping loose ranges in the project declaration, and make the environment reproducible from a file rather than from memory.

ModuleNotFoundError is almost always this and not a missing package — the wrong interpreter is running. Checking sys.executable resolves it in seconds, and is the first thing to check rather than the last.

bash
# Which interpreter is actually running, and what can it see?
python -c "import sys; print(sys.executable); print(sys.path)"

# Modern, fast, and reproducible
uv venv && source .venv/bin/activate
uv pip install -r requirements.txt

# Run a tool without polluting the project environment
uvx ruff check .

Frequently Asked Questions

Does the GIL mean Python cannot do concurrency?
No — it means threads do not give parallelism for CPU-bound Python code. I/O-bound concurrency works well with threads or asyncio, and CPU-bound parallelism works with processes or native extensions that release the GIL. A free-threaded build without the GIL is also now an official option, though ecosystem support is still maturing.
Should I use type hints?
On anything that will be maintained, yes. They are not enforced at runtime, but with a static checker they catch a real class of bug before it ships and they make editors dramatically more useful. Add them at module boundaries first — function signatures and public classes — rather than annotating every local.
What is the difference between a list and a generator comprehension?
Brackets build a list immediately and hold all of it. Parentheses build a generator that produces items on demand and holds one. Use the generator when the result is consumed once and may be large; use the list when you need to iterate it more than once, index it, or know its length.
Why is my Python code slow?
Usually an algorithmic problem, an unexpected N+1 of some kind, or a tight numeric loop in pure Python. Profile before optimising — cProfile for where the time goes, then look for repeated work. If the answer is a numeric loop, the fix is nearly always to express it as an array operation rather than to micro-optimise the loop.
pip, poetry or uv?
uv if you are starting now: it is dramatically faster, handles environments and lock files, and is a drop-in for most pip workflows. Poetry is a mature choice with strong dependency resolution. Plain pip with a requirements file is fine for simple projects. What matters more than the choice is that the environment is reproducible from a committed file.
What should I learn after the basics?
The data model — __init__, __repr__, __eq__, __iter__, context managers. It is the layer that turns Python from a scripting language into one where your own objects behave like built-ins, and it makes the standard library legible rather than magical.

Python Basics

Control Flow

Data Structures

Functions

OOP in Python

Exception Handling

File Handling

Python Libraries

NumPy Basics Intermediate NumPy Arrays and Operations Intermediate Pandas Basics Intermediate Pandas DataFrames Intermediate Matplotlib Basics Intermediate Seaborn for Data Visualisation Intermediate Requests Library in Python Intermediate Flask Web Framework Basics Intermediate Django Web Framework Basics Intermediate SQLAlchemy Basics Intermediate itertools Module in Python Advanced collections Module in Python Intermediate datetime Module in Python Beginner regex Module in Python Intermediate threading and multiprocessing in Python Advanced FastAPI Basics Intermediate Celery for Task Queues in Python Advanced Pytest Fixtures Intermediate Beautiful Soup Web Scraping Intermediate Selenium with Python Intermediate Pydantic for Data Validation Intermediate Streamlit for Data Apps Intermediate Playwright Python — Browser Automation and Testing Intermediate Advanced Network Interception and Mocking in Playwright Python Advanced NumPy Broadcasting — How It Actually Works Intermediate NumPy Indexing and Slicing — Beyond the Basics Intermediate NumPy Shape Manipulation — reshape, flatten, ravel, transpose Intermediate NumPy Mathematical Functions — ufuncs, aggregations and statistics Intermediate NumPy Random Module — Generating and Controlling Random Data Intermediate NumPy Linear Algebra — dot, matmul, linalg explained Advanced NumPy Boolean Indexing and Fancy Indexing Intermediate NumPy Performance Tips — Vectorisation vs Loops Advanced NumPy with Pandas — How They Work Together Intermediate NumPy dtype and Memory Layout — float32, int64 and C vs F order Advanced NumPy loadtxt and savetxt — Reading and Writing Array Data Beginner NumPy where, select and piecewise — Conditional Array Operations Intermediate FastAPI Path Parameters and Query Parameters Beginner FastAPI Request Body and Pydantic Models Beginner FastAPI Response Models and Status Codes Intermediate FastAPI Dependency Injection — How and Why to Use It Intermediate FastAPI Authentication — JWT and OAuth2 with Password Flow Intermediate FastAPI Background Tasks and Async Endpoints Intermediate FastAPI Database Integration with SQLAlchemy Intermediate FastAPI File Uploads and Form Data Intermediate FastAPI Middleware — Logging, CORS and Custom Middleware Intermediate FastAPI Testing with pytest and TestClient Intermediate FastAPI WebSockets — Real-time Communication Advanced FastAPI Deployment — Docker, Uvicorn and Gunicorn Advanced FastAPI vs Flask vs Django — When to Use Which Beginner FastAPI Error Handling and Custom Exception Handlers Intermediate PySpark Tutorial: Big Data Processing with Python Intermediate FastAPI CLI — dev, run and deploy Intermediate FastAPI OpenAPI Customization — Tags, Examples and Schema Intermediate FastAPI Lifespan Events — Startup, Shutdown and Context Managers Intermediate FastAPI Streaming Responses and File Responses Intermediate FastAPI Async SQLAlchemy — Alembic Migrations and Production Patterns Advanced FastAPI Cloud — Official Deployment Platform Intermediate

Advanced Python

Packaging

Tooling

Libraries

Basics

Errors

Web

Interview prep
→Python Interview Questions
Also Explore
ML / AI 206 tutorials → DSA 197 tutorials → C / C++ 66 tutorials → Interview 77 tutorials → Data Engineering 13 tutorials → Data Analytics 15 tutorials →
Start from the beginning

Every tutorial starts with a plain-English analogy — then real code, then interview questions.

Browse Python Tutorials →