So you’ve got a Python interview coming up? I’ve been there, and I know that feeling of wanting to make sure you really understand the fundamentals. Over the years, I’ve noticed the same questions keep popping up in interviews, and honestly, they’re asked for good reasons—they reveal how well you actually understand Python, not just whether you can Google syntax.
Let me walk you through the questions I see most often, along with explanations that actually make sense (no textbook jargon, I promise).
1. What Makes Python… Python?
You know what’s funny? This seems like such a basic question, but it’s actually the interviewer’s way of seeing if you understand why we use Python in the first place.
Here’s the thing—Python was designed to be readable. Like, really readable. When Guido van Rossum created it, he wanted code that you could almost read like English. And it worked! You don’t need semicolons everywhere, you don’t need to declare types for every variable, and honestly, after coding in Python, going back to other languages feels a bit verbose.
Python runs your code line by line (that’s the “interpreted” part), which makes debugging so much easier. You can literally open up a terminal, type python, and start playing around. No compilation step, no ceremony.
The dynamic typing thing? That means you can just write x = 5 and later x = "hello" And Python’s totally cool with it. Some people find this scary, but I find it liberating for rapid prototyping.
2. Lists vs Tuples—What’s the Big Deal?
Okay, so this one trips people up because lists and tuples look so similar. But trust me, the difference matters.
Think of it this way: a list is like a shopping list you can edit—cross things off, add new items, rearrange stuff. A tuple is like a GPS coordinate—once it’s (40.7128, -74.0060), it stays that way.
my_list = [1, 2, 3] # Square brackets = changeable
my_tuple = (1, 2, 3) # Parentheses = permanent
my_list[0] = 10 # Totally fine
# my_tuple[0] = 10 # Nope! Python throws a TypeError
Lists are packed with methods—you can append, remove, extend, you name it. Tuples? Not so much. They’re simpler, faster, and use less memory precisely because they can’t change.
I use tuples when I want to make it crystal clear that this data shouldn’t be modified. Like coordinates, RGB color values, or database records. It’s a signal to other developers (or future me) that says, “hey, don’t mess with this.”
3. The GIL—Python’s Weird Little Secret
Alright, this is where things get interesting. The Global Interpreter Lock (GIL) is one of those things that sounds way scarier than it actually is, but you should definitely understand it.
Here’s the deal: Python has this lock that says “only one thread can execute Python code at a time.” Yes, even on your fancy 16-core processor. I know, I know—it sounds crazy in 2025, right?
But here’s why it’s not actually the apocalypse: if your code is waiting around for stuff (downloading files, reading from disk, making API calls), the GIL doesn’t really matter. While one thread waits, another can run. It’s only when you’re crunching numbers non-stop that you run into issues.
When I hit GIL limitations, here’s what I do:
- For heavy number crunching, I use multiprocessing, which creates totally separate Python processes (each with their own GIL)
- For I/O stuff like web scraping, asyncio works beautifully
- Libraries like NumPy actually release the GIL during their computations, which is pretty neat
The GIL exists because it made Python’s memory management so much simpler to implement. Is it perfect? No. But it’s a trade-off that’s worked out pretty well for most use cases.
4. __str__ vs __repr__—Two Sides of the Same Coin
This one’s actually kind of elegant once you get it. These methods control how your objects look when you print them, but they’re meant for different audiences.
__str__ is for humans. It’s what shows up when you print something or convert it to a string. Make it friendly and readable.
__repr__ is for developers. It should give you enough info to recreate the object. When you’re debugging at 2 AM, you’ll thank yourself for a good __repr__.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
person = Person("Alice", 30)
print(str(person)) # Alice, 30 years old (friendly!)
print(repr(person)) # Person('Alice', 30) (useful for debugging!)
Pro tip: if you only define __repr__, Python will use it for both. But if you care about user experience, define both.
5. Decorators—Fancy Function Wrappers
Decorators sound intimidating, but they’re actually just a clean way to wrap functions with extra behavior. Once you “get” them, you’ll start seeing uses everywhere.
Think of it like gift wrapping. You have a present (your function), and you wrap it in paper (the decorator) that adds something extra—maybe timing, logging, or checking permissions.
def timer_decorator(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end-start:.2f} seconds")
return result
return wrapper
@timer_decorator
def slow_function():
import time
time.sleep(1)
return "Done"
That @timer_decorator line? It’s just Python shorthand. Without it, you’d write slow_function = timer_decorator(slow_function), which is way less pretty.
I use decorators all the time for things like checking if a user is logged in, caching expensive function calls, or (like above) timing how long things take. They keep your actual function code clean while adding functionality.
6. List Comprehensions vs Generators—Memory Matters
Here’s a real-world scenario: you need to process a million records. Do you load them all into memory at once, or process them one at a time? That’s basically the list vs generator question.
squares_list = [x**2 for x in range(1000)] # Boom! All in memory
squares_gen = (x**2 for x in range(1000)) # Creates a recipe, not the actual values
The difference? That list comprehension (square brackets) creates all 1000 numbers immediately and stores them. The generator (parentheses) creates them lazily, one at a time, as needed.
I learned this the hard way when I tried to load a 2GB CSV file into a list and watched my program crash. Switching to a generator? Smooth sailing.
Use list comprehensions when you need the entire list, such as to access items randomly or iterate multiple times. Use generators when you’re dealing with large datasets or when you only need to go through the data once. Your RAM will thank you.
7. Static Methods vs Class Methods—The Identity Crisis
These two decorators confused me for the longest time, so let me break it down in a way that finally made sense to me.
class MyClass:
class_var = "I belong to the class"
@staticmethod
def static_method():
return "I'm just a regular function living in this class"
@classmethod
def class_method(cls):
return f"I can access {cls.class_var}"
def instance_method(self):
return "I need an actual object to work"
Static methods are like the roommate who keeps to themselves—they don’t care about the class or any instances. They’re just utility functions that happen to live there because they’re related. Think of them as helper functions.
Class methods get passed the class itself (that cls parameter), so they can access class-level stuff and are great for alternative constructors. Like if you want to create a Person form from a date of birth instead of an age.
Regular instance methods need a specific object because they work with that object’s data.
Honestly? I rarely use static methods. If I don’t need the class or instance, why put it in the class? But class methods? Super useful for factory patterns.
8. Memory Management—Python’s Got Your Back
One of Python’s best features? You don’t spend your days hunting memory leaks like it’s a C program from the ’90s. But you should still understand what’s happening under the hood.
Python uses reference counting as its first line of defense. Every object keeps track of how many variables point to it:
x = [1, 2, 3] # List created, ref count = 1
y = x # Same list, ref count = 2
del y # Back to 1
del x # Hits 0, memory freed instantly
Simple, right? But here’s where it gets tricky—circular references. If two objects reference each other, their counts never hit zero:
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # Uh oh, they're stuck together
That’s where the generational garbage collector comes in. Python periodically scans for these orphaned cycles and cleans them up. It’s called “generational” because it assumes most objects die young, so it checks new objects more frequently.
The best part? You rarely need to think about any of this. Python handles it. But in interviews, knowing this shows you understand what’s happening behind the scenes.
9. *args and **kwargs—Python’s Flexible Friends
These look weird at first, but they’re incredibly useful once you get comfortable with them. The asterisks are what matter—the names “args” and “kwargs” are just conventions.
def example_function(a, b, *args, **kwargs):
print(f"Required: {a}, {b}")
print(f"Extra positional stuff: {args}")
print(f"Extra keyword stuff: {kwargs}")
example_function(1, 2, 3, 4, 5, name="Alice", age=30)
# Required: 1, 2
# Extra positional stuff: (3, 4, 5)
# Extra keyword stuff: {'name': 'Alice', 'age': 30}
Think of *args as “pack any extra positional arguments into a tuple” and **kwargs as “pack any extra keyword arguments into a dictionary.”
Real talk? I use this pattern constantly:
- When wrapping other functions (decorators!)
- When building APIs that need to be flexible
- When I don’t know exactly what arguments someone might pass
Here’s a practical example:
def make_pizza(size, *toppings, **details):
print(f"Making a {size} pizza with {', '.join(toppings)}")
if details.get('extra_cheese'):
print("Adding EXTRA cheese!")
make_pizza("large", "pepperoni", "mushrooms", extra_cheese=True, delivery=True)
See? Clean and flexible.
10. Magic Methods—Making Your Objects Feel Native
Magic methods (or “dunder” methods—double underscore) are what make Python feel like Python. They let your custom objects play nicely with built-in operations.
Want to add two of your objects with +? Define __add__. Want them to work with len()? Define __len__. It’s like teaching your objects to speak Python fluently.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2) # Vector(6, 8) - so natural!
The first time I made a class that worked with + and -, I felt like a wizard. Instead of calling v1.add(v2), I could just write v1 + v2. It makes your code so much more readable.
Some magic methods you’ll use all the time:
__init__: Constructor (you know this one)__str__and__repr__: String representations__len__: Makeslen(obj)work__getitem__: Makesobj[key]work__eq__: Makes==work- Arithmetic ones like
__add__,__mul__, etc.
They’re called “magic” but there’s nothing mysterious—they’re just hooks into Python’s syntax.
Bonus: Context Managers—Your Safety Net
If you want to impress an interviewer, bring up context managers. They’re the with statement under the hood, and they’re all about making sure cleanup happens, no matter what.
You’ve used them without realizing it:
with open('file.txt', 'r') as f:
data = f.read()
# File is DEFINITELY closed here, even if something went wrong
But you can make your own! Here’s the pattern:
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
if exc_type:
print(f"Oops, something went wrong: {exc_type}")
return False # Don't suppress exceptions
with FileManager("test.txt", "w") as f:
f.write("Hello World")
The __enter__ method runs when you enter the with block. The __exit__ method runs when you leave it—whether you left normally or because an exception was raised.
This pattern is gold for managing database connections, locks, or any resource that needs cleanup. I sleep better at night knowing my database connections will close even if my code crashes.

