Zum Hauptinhalt springen

Chapter 1 — Python Philosophy and the Pythonic Mental Model

1. Why an experienced TypeScript developer needs this chapter

An experienced TypeScript developer can learn basic Python syntax in a few days. That is not the difficult part.

The difficult part is resisting the temptation to write TypeScript-style software using Python syntax.

Consider this Python function:

def find_user(users, user_id):
for user in users:
if user["id"] == user_id:
return user
return None

A TypeScript developer may immediately want to add:

  • an interface for the user;
  • a class for the collection;
  • a service layer;
  • a repository abstraction;
  • explicit getter methods;
  • several guard clauses;
  • a generic result wrapper.

Those tools can be useful. However, Python culture asks a different first question:

What is the simplest readable implementation that correctly expresses the domain?

Python does not reject architecture, static typing, or abstraction. It discourages abstraction that does not yet pay for itself.

This chapter builds the philosophical foundation for the entire book.


2. Python is not merely a language

Python is simultaneously:

  • a programming language;
  • a runtime model;
  • a standard library;
  • a packaging ecosystem;
  • a design culture;
  • a collection of conventions.

The word Pythonic describes code that fits the language's conventions and mental models.

Pythonic does not mean:

  • using the shortest possible syntax;
  • using every clever language feature;
  • avoiding classes;
  • avoiding type annotations;
  • ignoring performance;
  • refusing architecture.

Pythonic code usually means:

  • readable;
  • unsurprising;
  • direct;
  • based on established protocols;
  • appropriately explicit;
  • simple enough for the problem;
  • easy to test and maintain.

3. The central philosophical principles

3.1 Readability is a technical property

Readability is not decoration. It affects:

  • debugging time;
  • onboarding;
  • review quality;
  • defect rates;
  • maintenance cost;
  • incident response;
  • architectural flexibility.

Compare:

result = [u.email.lower() for u in users if u.active and u.email]

with:

active_email_addresses = []

for user in users:
if not user.active:
continue

if user.email is None:
continue

active_email_addresses.append(user.email.lower())

The first version is compact. The second version is longer but may be easier to extend when validation, logging, or metrics are added.

Pythonic code is not always the shortest code. It is the code whose structure best communicates the intention.

3.2 Explicit is better than implicit

This does not mean everything must be verbose.

It means important behaviour should be visible.

For example:

from pathlib import Path

config_path = Path("config") / "app.toml"

is preferable to constructing paths manually:

config_path = "config/" + "app.toml"

The first form explicitly uses a path abstraction.

Another example:

def connect(*, timeout_seconds: float = 5.0) -> None:
...

The * forces callers to name the argument:

connect(timeout_seconds=10.0)

That call is more explicit than:

connect(10.0)

3.3 Simple is better than complex

Suppose a feature needs to map external status values:

STATUS_MAP = {
"created": "pending",
"processing": "active",
"done": "completed",
}

A complex strategy-pattern hierarchy may be unnecessary.

Simple does not mean primitive. It means using the least complicated design that preserves correctness and future change.

3.4 Complex is better than complicated

Some domains are genuinely complex:

  • payments;
  • taxation;
  • distributed workflows;
  • security;
  • healthcare;
  • scheduling.

The goal is not to pretend complexity does not exist. The goal is to prevent complexity from becoming accidental complication.

A clear state machine may be complex but understandable. A collection of unrelated boolean flags may be less code but more complicated.

3.5 Errors should not pass silently

This code hides every error:

try:
process_order(order)
except Exception:
pass

That is dangerous because it destroys operational information.

A better design catches only errors it can handle:

try:
process_order(order)
except PaymentDeclinedError as error:
mark_order_as_payment_failed(order, reason=str(error))

Unexpected errors should usually be logged and allowed to propagate to an appropriate boundary.


4. EAFP and LBYL

Two common approaches to defensive programming are:

  • LBYL: Look Before You Leap
  • EAFP: Easier to Ask Forgiveness than Permission

4.1 LBYL

if "email" in payload:
email = payload["email"]
else:
email = None

4.2 EAFP

try:
email = payload["email"]
except KeyError:
email = None

EAFP is common in Python because many operations naturally report failure through exceptions.

However, EAFP is not automatically better.

Use LBYL when:

  • the check is cheap;
  • the check itself communicates business meaning;
  • failure is expected and common;
  • the operation has side effects;
  • repeating the operation would be unsafe.

Use EAFP when:

  • the operation is the most reliable test;
  • the state can change between checking and acting;
  • exceptions are precise and expected;
  • a preliminary check would duplicate work.

4.3 Race-condition example

This is vulnerable to a time-of-check/time-of-use problem:

from pathlib import Path

path = Path("report.txt")

if path.exists():
content = path.read_text()

The file may disappear after exists() returns True.

EAFP handles the operation directly:

from pathlib import Path

path = Path("report.txt")

try:
content = path.read_text()
except FileNotFoundError:
content = ""

5. Duck typing

Duck typing means that code focuses on supported behaviour rather than concrete inheritance.

def save_all(repository, entities) -> None:
for entity in entities:
repository.save(entity)

The function does not require repository to inherit from a specific base class. It requires only a compatible save operation.

In modern production Python, duck typing can be combined with static analysis:

from typing import Protocol, TypeVar

T = TypeVar("T")


class Repository(Protocol[T]):
def save(self, entity: T) -> None:
...


def save_all(repository: Repository[T], entities: list[T]) -> None:
for entity in entities:
repository.save(entity)

This is close to a TypeScript structural interface:

interface Repository<T> {
save(entity: T): void;
}

The philosophical difference is historical:

  • Python began with runtime duck typing.
  • TypeScript added static structural typing to JavaScript.
  • Python type hints later added static descriptions to an already dynamic language.

6. Protocols over rigid inheritance

Python's object model is built around protocols.

An object can participate in an operation when it provides the expected special methods.

Examples:

  • len(value) calls a length protocol;
  • iteration uses the iterable and iterator protocols;
  • with uses the context-manager protocol;
  • + uses arithmetic special methods;
  • membership testing uses containment or iteration behaviour.
class Team:
def __init__(self, members: list[str]) -> None:
self._members = members

def __len__(self) -> int:
return len(self._members)


team = Team(["Mika", "Lily", "Max"])
print(len(team))

The object works with len() because it participates in the protocol.

This encourages behavioural integration instead of framework-specific inheritance.


7. Prefer intention-revealing operations

TypeScript developers often use index-based loops because they are familiar:

for (let index = 0; index < users.length; index++) {
console.log(users[index]);
}

Direct Python translation:

for index in range(len(users)):
print(users[index])

More idiomatic Python:

for user in users:
print(user)

When the index is needed:

for index, user in enumerate(users):
print(index, user)

When two collections are traversed together:

for user, score in zip(users, scores):
print(user, score)

The operation should describe the intention:

  • iterate over elements;
  • enumerate;
  • combine;
  • sort;
  • group;
  • filter;
  • map.

8. "There should be one obvious way" is guidance, not law

Python often provides several ways to solve a problem:

squares = []

for number in range(10):
squares.append(number * number)
squares = [number * number for number in range(10)]
squares = list(map(lambda number: number * number, range(10)))

All are valid. In ordinary Python code, the comprehension is often the clearest.

But when logic grows:

normalized_users = [
normalize(user)
for user in users
if user.active and user.email and validate_email(user.email)
]

a normal loop may become clearer.

The obvious way depends on the complexity of the operation and the expectations of the team.


9. Common TypeScript-to-Python mistakes

Mistake 1: Creating an interface for every object

Python does not require a formal interface for every collaboration.

Use a protocol when static documentation or multiple implementations justify it. Do not add one mechanically.

Mistake 2: Writing Java-style getters and setters

Avoid:

class User:
def __init__(self, email: str) -> None:
self._email = email

def get_email(self) -> str:
return self._email

def set_email(self, email: str) -> None:
self._email = email

Prefer a public attribute when no special behaviour is required:

class User:
def __init__(self, email: str) -> None:
self.email = email

Add a property later if validation becomes necessary:

class User:
def __init__(self, email: str) -> None:
self.email = email

@property
def email(self) -> str:
return self._email

@email.setter
def email(self, value: str) -> None:
normalized = value.strip().lower()

if "@" not in normalized:
raise ValueError("Invalid email address")

self._email = normalized

The public API remains user.email.

Mistake 3: Overusing inheritance

Prefer small collaborating objects and protocols.

Mistake 4: Translating undefined directly

Python has None, but absence, missing input, and explicit null values may need separate modelling with sentinel objects.

Mistake 5: Expecting annotations to enforce runtime types

They usually do not.

Mistake 6: Hiding all exceptions

Catch only exceptions that can be handled meaningfully.


10. Production case study: configuration loading

Overengineered version

class ConfigReaderInterface:
def read(self):
raise NotImplementedError


class EnvironmentConfigReader(ConfigReaderInterface):
def read(self):
...

This may be justified in a large system, but often a function is enough:

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
database_url: str
debug: bool


def load_settings() -> Settings:
database_url = os.environ["DATABASE_URL"]
debug = os.getenv("DEBUG", "").lower() in {"1", "true", "yes"}

return Settings(
database_url=database_url,
debug=debug,
)

The design is:

  • explicit;
  • testable;
  • small;
  • typed;
  • immutable after construction;
  • easy to replace later.

A unit test can temporarily patch environment values or extract parsing into a pure function.


11. English for professional Python discussions

Core vocabulary

TermMeaning
readabilityhow easily code can be understood
explicitdirectly visible rather than hidden
implicitunderstood indirectly from context
conventionan agreed way of doing something
abstractiona simplified model hiding lower-level details
accidental complexitycomplexity caused by the solution rather than the domain
idiomaticnatural and conventional for a language
maintainabilityhow easily software can be changed safely
trade-offa choice that improves one property while weakening another
protocola set of behaviours an object is expected to support

Useful sentences

  • "This implementation is valid, but it is not especially idiomatic."
  • "The abstraction does not yet justify its maintenance cost."
  • "I would prefer a protocol over a shared base class."
  • "The code is concise, but the intention is difficult to read."
  • "This exception should be handled at the application boundary."
  • "The design removes accidental complexity without hiding domain complexity."

12. Speaking task

Speak for five minutes without reading:

Explain what Pythonic code means to a TypeScript developer.

Your explanation should include:

  • readability;
  • explicitness;
  • simplicity;
  • duck typing;
  • protocols;
  • EAFP;
  • the danger of overengineering.

Record yourself. Listen again and identify:

  • unclear sentences;
  • repeated filler words;
  • missing technical terms;
  • grammar mistakes;
  • places where an example would help.

13. Writing task

Write a 250-word code-review comment responding to this design:

class EmailGetterInterface:
def get_email(self) -> str:
raise NotImplementedError


class CustomerEmailGetter(EmailGetterInterface):
def __init__(self, customer):
self.customer = customer

def get_email(self) -> str:
return self.customer.get_email()

Your review should be constructive. Explain when the abstraction might be useful and when direct access would be simpler.


14. Exercises

Exercise 1: Choose the clearest version

Which implementation is clearer for a simple transformation?

result = []

for item in items:
if item.active:
result.append(item.name.upper())

or:

result = [item.name.upper() for item in items if item.active]

Explain your decision and describe a situation where the other version would become preferable.

Exercise 2: EAFP or LBYL?

Design a function that reads a cached value from a dictionary. If the key does not exist, calculate the value and store it.

Write both an LBYL and an EAFP version.

Exercise 3: Refactor the loop

Refactor:

for index in range(len(users)):
print(index, users[index].email)

Exercise 4: Reduce accidental complexity

Refactor:

class ActiveUserEmailCollector:
def __init__(self, users):
self.users = users

def execute(self):
result = []
for user in self.users:
if user.active is True:
if user.email is not None:
result.append(user.email.lower())
return result

Exercise 5: Explain the trade-off

Explain why the following code may be too broad:

try:
create_invoice(order)
except Exception:
return None

15. Complete solutions

Solution 1

For a simple one-step transformation, the comprehension is concise and readable:

result = [item.name.upper() for item in items if item.active]

The loop becomes preferable when:

  • several conditions need comments;
  • errors are handled per item;
  • metrics or logging are added;
  • intermediate values need descriptive names;
  • the transformation has several stages.

The decision is based on readability, not character count.

Solution 2

LBYL:

def get_or_calculate(cache, key, calculate):
if key not in cache:
cache[key] = calculate()

return cache[key]

EAFP:

def get_or_calculate(cache, key, calculate):
try:
return cache[key]
except KeyError:
value = calculate()
cache[key] = value
return value

The EAFP version directly attempts the operation. The LBYL version may be easier for beginners to read. For an ordinary in-memory dictionary, both can be acceptable.

Solution 3

for index, user in enumerate(users):
print(index, user.email)

enumerate directly communicates that both the position and the element are required.

Solution 4

A simple function is sufficient:

def collect_active_user_emails(users):
return [
user.email.lower()
for user in users
if user.active and user.email is not None
]

With static types:

from collections.abc import Iterable
from dataclasses import dataclass


@dataclass
class User:
email: str | None
active: bool


def collect_active_user_emails(users: Iterable[User]) -> list[str]:
return [
user.email.lower()
for user in users
if user.active and user.email is not None
]

A class may become useful if the operation later needs dependencies, configuration, or lifecycle state.

Solution 5

Catching Exception treats expected and unexpected failures identically.

Possible hidden failures include:

  • programming errors;
  • database connectivity problems;
  • invalid configuration;
  • serialization defects;
  • authentication problems.

A better version catches only a domain error it can handle:

try:
return create_invoice(order)
except UnsupportedInvoiceAddressError:
return None

Unexpected failures remain visible and can be handled by logging, retry, or error-reporting infrastructure at a higher boundary.


16. Chapter checkpoint

You are ready to continue when you can explain:

  1. Why Pythonic code is not always the shortest code.
  2. Why readability is an engineering property.
  3. The difference between EAFP and LBYL.
  4. Why protocols often fit Python better than rigid inheritance.
  5. Why a TypeScript architecture should not be translated mechanically.
  6. When abstraction is useful and when it creates accidental complexity.