The book that turns a dev who writes Python into a dev who thinks in Python.
Why this book
There are dozens of books for learning Python. Fluent Python is not one of them: Ramalho assumes you already write Python, and tackles the next problem. Python that works but smells like translated Java, disguised PHP, copy-pasted JS. His angle: showing you how the language thinks, so your code speaks the same language as the interpreter.
The preface announces the method: use before you build. You use ABCs before writing one, you live with metaclasses before creating any. And a warning that sets the tone: "Premature abstraction is as bad as premature optimization" (preface).
The ideas that stay
1The iceberg: the Python Data Model
Why len(obj) and not obj.len()? Why does obj[key] trigger __getitem__? Because Python rests on a contract: the special methods (the "dunders": __len__, __getitem__...) that the interpreter calls for you. "The iceberg is called the Python Data Model, and it is the API that we use to make our own objects play well with the most idiomatic language features" (p. 3). And why isn't len() a method? Raymond Hettinger's answer, quoted in the book: for built-in types, CPython reads a C struct field directly, with no method call. "Practicality beats purity." The whole book flows from that logic: the language favors consistent practicality over theoretical purity.
2Two methods, six capabilities for free
The opening example, FrenchDeck, is a 52-card deck in 15 lines. It implements only __len__ and __getitem__. As a result: len(deck), deck[0], slicing like deck[12::13] (the four aces), the for loop, the in operator and sorted() all work, with no inheritance and no declared interface. That's the return on investment of the Data Model: implement the contract, the language does the rest.
class FrenchDeck: def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position] >>> len(deck) # 52 >>> deck[12::13] # the four aces >>> Card('Q', 'hearts') in deck # True
3Variables are labels, not boxes
The most common Python trap comes from a wrong mental image. "The b = a statement does not copy the contents of box a into box b. It attaches the label b to the object that already has the label a" (p. 203). Hence chapter 6's HauntedBus: a mutable default parameter (def __init__(self, passengers=[])) is created once and shared by every instance built without arguments. Ghost passengers from one bus haunt all the buses created without passengers.
4Closures: the function carries its environment
make_averager() returns a function computing a running average. The series list is a local variable of make_averager, yet the returned function still accesses it long after. That's a closure: "a function that retains the bindings of the free variables that exist when the function is defined" (p. 314). A subtlety the book takes apart: reassigning (count += 1) breaks the closure, because assignment makes the variable local. The nonlocal keyword exists for exactly this.
5Decorators and their two timelines
A decorator is target = decorate(target), nothing more. But chapter 9 installs a distinction that changes how you read code: decorators run at module import, decorated functions run when called. And the showcase: naive recursive fibonacci(30) calls fibonacci(1) 832,040 times (12 seconds); with @functools.cache, one line above the definition, 31 calls and 0.00017 seconds.
6Duck typing, goose typing: the Typing Map
Since Python 3.8 there are four ways to think about interfaces, which the book maps along two axes: runtime vs static checking, structural vs name-based typing. Classic duck typing (the object has the method, so it works: the Vowels class with a single __getitem__ is iterable), goose typing (ABCs + isinstance), static typing (Java-style type hints), and static duck typing (typing.Protocol, Go's approach). The book's verdict: these four approaches are complementary, and none deserves to be dismissed (p. 432). The map is there to help you choose deliberately: duck typing for a script, Protocol for a public library, ABCs when you need runtime checks.
7yield: the Iterator pattern built into the language
When data doesn't fit in memory, you need to produce items one at a time, on demand. Where Java implements the Iterator design pattern by hand, Python built it in: any function containing yield becomes a generator factory. A vocabulary precision the book hammers: a function returns a value, a generator yields values. The lazy version of the Sentence class (via re.finditer) only reads the next word when you ask for it.
8The GIL, explained in ten points
Chapter 19 defuses Python's most misunderstood topic: only one Python thread executes at a time, regardless of CPU cores. But every function that makes a syscall (disk I/O, network, time.sleep) releases the GIL. Consequence: Python threads are excellent at waiting ("Python threads are great at doing nothing", David Beazley, quoted p. 700) and useless for computation. A lesser-known detail from point 6: many CPU-intensive NumPy/SciPy functions and the zlib/bz2 compressors also release the GIL, which is why threaded scientific computing can work. For CPU-intensive pure Python: multiple processes.
Three things I didn't know before reading it
- The entire asyncio documentation was re-tagged in under 12 hours after a single mailing-list post by Ramalho. Victor Stinner, Ben Darnell (Tornado) and Glyph Lefkowitz (Twisted) joined the thread, and the fix was live the same evening (Afterword, p. 959). His conclusion: the community is the best part of the ecosystem.
- "Python is a language for consenting adults" (Alan Runyan, quoted at the top of the Afterword): no real
private, nofinal. The language doesn't protect you from yourself, and that's a deliberate design choice. - The book is verifiable: every console session is a doctest.
python3 -m doctest example.pyreplays all the examples. 1,011 pages of code that actually runs.
My take, honestly
What makes the book unique is its single thread. Other Python books list features; this one unrolls one idea (the Data Model) across five parts, and every chapter confirms it. The examples have names (FrenchDeck, HauntedBus) you remember years later.
The deserved criticism: 1,011 pages is a marathon, and not everyone will finish. The type-hint chapters (8 and 15) are dry. Part V (metaprogramming) concerns maybe 5% of readers. Ramalho knows it: he structured the book as "five books in one", to be read by parts. Treat it as a six-month bedside book, not a one-week read.
In 2026, AI generates Python that works, rarely Python that's idiomatic: useless getters and setters, index-based loops where a comprehension would do, threading where the GIL cancels it out. Fluent Python is the grid that lets you see the difference between Python and Java translated into Python.
Odilon
Still relevant in 2026?
Second edition from 2022, covering Python 3.10: pattern matching and modern typing are in. The optional-GIL project (PEP 703, Python 3.13+) doesn't change chapter 19's ten points yet: the GIL remains on by default. The fundamentals (Data Model, references, closures) don't move, because they are the language itself.
Who is it for?
Read it if
- You have 1-2 years of Python and your programs work without you always knowing why
- You come from Java, PHP or JS and your Python still looks like your old language
- You review AI-generated Python and want to spot what isn't idiomatic
- You want to understand the
__dunders__instead of copying them from Stack Overflow
Skip it if
- You're new to programming: Python Crash Course first, this one in two years
- You're looking for recipes to paste: this is a mental-model book, not a cookbook
- You use Python occasionally for scripts: the return on 1,011 pages won't be there
For going further
The fundamentals practiced in this book pair with the Python course on this site. For reviewing AI-generated code with a critical eye, see Coding with AI. The TypeScript equivalent of this book's role is Effective TypeScript, also in this library.
Comments (0)