Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
454: Common Python Interview Questions and Gotchas
If you've spent any time in technical interviews, you know there's a specific category of Python questions designed not to see if you can code, but to see if you've been "burned" by the language's quirks. I call these "gotchas." They aren't usually about complex algorithms; they're about the subtle ways Python handles memory and scope that defy your intuition.
To walk through these, let's build a simple TaskRegistry. The goal is a system where we can register tasks and create small "handler" functions to execute them. It sounds trivial, but it's the perfect playground for the three most common interview traps.
Setting up our Task Manager with a hidden bug
I'll start by writing a function to add a task to a list. I want it to be flexible, so if the user doesn't provide a list, I'll just start a new one. It feels natural to do this:
def add_task(task_name, task_list=[]):
task_list.append(task_name)
return task_list
# First call
print(add_task("Email Client")) # Expected: ['Email Client']
# Second call
print(add_task("Database Backup")) # Expected: ['Database Backup']
When I run this, I get ['Email Client'] followed by ['Email Client', 'Database Backup']. Wait, what? I called the function twice with no list provided, so I expected two separate lists. This is the most famous Python interview question: Mutable Default Arguments.
In Python, default arguments are evaluated once at definition time, not every time the function is called. That [] isn't a blueprint for a new list; it's a single list object created when the script starts. Every call to add_task that relies on the default is sharing that exact same list in memory. It's a memory leak waiting to happen in a production app.
Cleaning up the shared state
The professional way to handle this—and the answer the interviewer is looking for—is to use None as a sentinel value. I'll rewrite it to ensure a fresh list is created during the function's execution, not at its definition.
def add_task(task_name, task_list=None):
if task_list is None:
task_list = []
task_list.append(task_name)
return task_list
# Now it behaves:
print(add_task("Email Client")) # ['Email Client']
print(add_task("Database Backup")) # ['Database Backup']
By moving the initialization inside the function body, we ensure that task_list = [] runs every time the function is invoked without an argument.
The late-binding trap in loop-generated functions
Now, let's say we want to create a set of "trigger" functions for our tasks. I want a list of functions where each function, when called, tells me which task ID it's associated with. I'll use a loop and a lambda to keep it concise.
task_ids = [101, 102, 103]
triggers = []
for tid in task_ids:
triggers.append(lambda: f"Executing task {tid}")
# Let's test them
for t in triggers:
print(t())
If you're guessing that this prints 101, 102, 103, you're falling for the same trap I did years ago. It actually prints "Executing task 103" three times. This is Late Binding.
The lambda doesn't capture the value of tid at the moment it's created; it captures the variable tid. By the time we actually call the functions, the loop has finished, and tid is left sitting at 103. Every single lambda is looking at the same variable in the outer scope.
Pinning the value to the local scope
To fix this, we need to force Python to bind the value at definition time. We can do this by passing tid as a default argument to the lambda. This feels a bit hacky, but it's the idiomatic way to solve the problem because default arguments are evaluated when the function is defined.
task_ids = [101, 102, 103]
triggers = []
for tid in task_ids:
# By setting tid=tid, we create a local variable for the lambda
triggers.append(lambda tid=tid: f"Executing task {tid}")
for t in triggers:
print(t()) # Now it correctly prints 101, 102, 103
One last tip: if an interviewer asks you about is vs ==, just remember that == checks for equality (do they have the same value?), while is checks for identity (are they literally the same object in memory?). For integers, Python caches small numbers (usually -5 to 256), so a = 10; b = 10; a is b might be True, but a = 1000; b = 1000; a is b will be False. Never use is for value comparison.
📋 Practical Task
Refactoring a Glitchy Event Dispatcher
You've been handed a piece of legacy code for an Event Dispatcher. It has two major bugs: it's accidentally sharing a history log across different dispatcher instances, and its event handlers are all returning the value of the very last event registered.
Your Task: Fix the following code so that each Dispatcher has its own unique history and each handler returns its correct event name.
class EventDispatcher:
def __init__(self, name, history=[]):
self.name = name
self.history = history
def register_events(self, events):
handlers = []
for e in events:
# BUG: Late binding issue here
handlers.append(lambda: f"Event {e} triggered in {self.name}")
return handlers
# Test Case
disp1 = EventDispatcher("AuthSystem")
disp2 = EventDispatcher("PaymentSystem")
disp1.register_events(["Login"])
disp2.register_events(["PaymentReceived"])
# This should print only ['PaymentReceived'], but currently prints ['Login', 'PaymentReceived']
print(f"Dispatcher 2 History: {disp2.history}")
# These should print "Event Login..." and "Event PaymentReceived..."
# but currently both print the last event.
h1 = disp1.register_events(["Login"])[0]
h2 = disp2.register_events(["PaymentReceived"])[0]
print(h1())
print(h2())
There are no comments for now.