I keep seeing Python explained either like it's magic or like it's a set of rules you must memorize to be worthy.

It's neither. Python is mostly a pile of trade offs that happen to work well together.

Some are elegant. Some are annoying. Some are both at the same time.

This isn't a tutorial.

It's me laying out how I think about Python's core ideas, what they're good at, and where they get in the way.

Read the full story for free here on Medium

None
Canva

The Zen of Python

If you run

import this

You get 19 aphorisms.

People treat them like scripture. They're not.

They're design preferences written down so the language doesn't drift into chaos.

Readability matters. Explicit is better than implicit.

Flat is better than nested. Clever code is usually bad code wearing a tuxedo.

The Zen isn't about writing pretty code. It's about making code obvious to the next person.

Which is often you, six months later.

if __name__ == "__main__"

This is probably the most memed line in Python.

def main():
    print("Program running")

if __name__ == "__main__":
    main()

__name__ is a special variable.

When a file is run directly, it's set to "__main__".

When the file is imported, it becomes the module's name.

This line is just a gate. Code below it only runs when the file is executed directly.

Not when it's imported.

It's not deep. It's just control.

Everything Is an Object

In Python, everything is an object. Not metaphorically. Literally.

x = 5
print(x.__add__(3))  # 8

Numbers are objects. Functions are objects.

Classes, modules, even types themselves.

def greet():
    return "hi"

print(type(greet))

This consistency makes Python flexible and predictable.

It also costs memory and speed. That's the trade.

Whitespace and Indentation

Python doesn't use braces. It uses indentation.

if True:
    print("inside")
    if True:
        print("nested")

Same indentation level means same block.

More indentation means nesting. Mess it up and Python will stop you immediately.

This forces readability. It also means you can't be sloppy.

That's intentional.

else in Loops

This one confuses people.

for n in range(5):
    if n == 10:
        break
else:
    print("Loop finished without break")

The else runs only if the loop didn't hit break.

It's not an if/else. It's a no break clause.

Useful for searches and validation loops. Weird, but practical.

List Comprehensions

Compact loops.

squares = [x * x for x in range(10)]

With conditions

evens = [x for x in range(10) if x % 2 == 0]

They're great until they're not.

Once they start wrapping lines and nesting logic, they stop being readable.

At that point, just write the loop.

Multiple Assignment and Tuple Unpacking

a, b = 1, 2

Python treats the right side as a tuple.

values = (3, 4)
x, y = values

You can unpack anything iterable.

for index, value in enumerate(["a", "b", "c"]):
    print(index, value)

Tuples themselves are immutable. Once created, they don't change.

Dynamic and Strong Typing

Python is dynamically typed.

x = 5
x = "hello"

But it's also strongly typed.

print(5 + "5")  # TypeError

Types are decided at runtime, but Python won't silently convert incompatible ones.

That saves you from subtle bugs.

Duck Typing

Python cares about behavior, not labels.

class Duck:
    def quack(self):
        print("quack")

class Dog:
    def quack(self):
        print("quack")
def make_it_quack(thing):
    thing.quack()

If it has the method, it works. No inheritance required.

This is powerful.

It's also how you get runtime errors if you're careless.

The pass Statement

class User:
    pass

pass does nothing. It exists so Python doesn't complain about empty blocks.

It's a placeholder. A skeleton. Nothing more.

First Class Functions and Closures

Functions are just objects.

def add(a, b):
    return a + b

operation = add
print(operation(2, 3))

Closures capture state.

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

This is where Python quietly becomes functional.

Dunder Methods

Double underscore methods run behind the scenes.

class Number:
    def __init__(self, value):
        self.value = value

def __add__(self, other):
        return Number(self.value + other.value)

You don't usually call them directly. Operators call them for you.

args and kwargs

def log(*args, **kwargs):
    print(args)
    print(kwargs)

args collects positional arguments. kwargs collects keyword arguments.

It's flexible. It can also hide bad APIs if overused.

The Walrus Operator

Assignment inside expressions.

while (line := input()) != "quit":
    print(line)

Useful sometimes. Overused quickly.

If it hurts readability, don't use it.

Decorators

Functions that modify functions.

def my_decorator(func):
    def wrapper():
        print("before")
        func()
        print("after")
    return wrapper

@my_decorator
def say_hi():
    print("hi")

They're powerful. They also make code harder to trace. Trade off.

with and Context Managers

with open("file.txt") as f:
    data = f.read()

Under the hood, this uses __enter__ and __exit__ to guarantee cleanup.

No try/finally. Fewer bugs.

__slots__

class User:
    __slots__ = ("name", "age")

This avoids per‑instance dictionaries and saves memory.

It also breaks introspection and flexibility. Don't use it unless you actually need it.

else in Error Handling

try:
    result = int("123")
except ValueError:
    print("error")
else:
    print("success", result)

The else runs only if no exception occurred.

It makes the success path explicit. That's all.

Mutable Default Arguments

The classic trap.

def add_item(item, items=[]):
    items.append(item)
    return items

That list persists across calls.

The fix

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Python isn't wrong here. It's just doing exactly what you told it to do.

The Global Interpreter Lock

The GIL ensures only one thread executes Python bytecode at a time.

This simplifies memory management and object safety.

It also limits CPU bound multithreading.

That's why Python leans on multiprocessing, async, and C extensions.

It's not a mistake. It's a design choice with consequences.

I don't think Python is perfect. I don't think it needs to be.

It's a language that makes certain trade offs and then sticks to them.

If you understand those trade offs, Python stops feeling magical.

And that's usually when it becomes useful.

None
Canva