Python Flashcards
(15 cards)
Explain *args and **kwargs in Python.
💡 Answer:
In Python, *args
and **kwargs
are used to pass a variable number of arguments to a function.
-
*args
collects extra positional arguments as a tuple. -
**kwargs
collects extra keyword arguments as a dictionary.
🔢 Use *args
for Positional Args
def sum_all(*args): return sum(args) sum_all(1, 2, 3, 4) # ➝ 10
🔑 Use **kwargs
for Named Args
def print_values(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}") print_values(name="Alice", age=25) # name: Alice # age: 25
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
Think of *args
and **kwargs
as flexible containers that let you pack extra arguments into a function — even when you don’t know how many you’ll get.
-
*args
is like saying: “Send me as many unnamed values as you want — I’ll treat them like a list.” -
**kwargs
says: “Send me any extra named values — I’ll treat them like a dictionary.”
They’re super useful when writing generic, reusable functions or decorators.
🔑 Câu nên nhớ (Highlight để học thuộc):
- ”
*args
→ collects positional args as a tuple” - ”
**kwargs
→ collects keyword args as a dict” - “Use them for flexibility and generic functions”
What is lambda function in Python?
💡 Answer:
A lambda function is a small, anonymous function in Python defined using the lambda
keyword. It’s best suited for quick, one-off tasks where writing a full def
function feels excessive.
🔍 Core Features
- Anonymous: No need to give it a name.
- Single Expression Only: No multiple lines or statements.
- Implicit Return: The expression is automatically returned.
- Compact: Makes simple logic concise and readable (when used well).
🛠️ Common Use Cases
-
map()
,filter()
,reduce()
doubled = list(map(lambda x: x * 2, [1, 2, 3])) # ➝ [2, 4, 6]
-
Custom sort key
names.sort(key=lambda name: len(name)) # Sort by length
- Callbacks in GUI/event systems
- Short inline logic in scripts or comprehensions
⚠️ Limitations
- Not good for complex logic
- Can hurt readability if overused
- No docstrings — harder to document
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
A lambda function is like a quick “throwaway” function — no name, just a one-liner to get something done fast.
It’s super handy when you need a tiny function for something like map
, filter
, or custom sorting.
Think of it as:
“Why bother writing a full function with def
just to square a number once?” — that’s where lambda shines.
But if it gets too long or unreadable, it’s better to switch to a named function.
🔑 Câu nên nhớ (Highlight để học thuộc):
- “
lambda
= tiny anonymous function for quick jobs” - “One expression only — no
return
, no name” - “Great for map, filter, sort, or callbacks”
What are decorators in Python?
💡 Answer:
In Python, a decorator is a design pattern that lets you dynamically modify or enhance functions or methods without changing their actual code. It’s a powerful way to keep your code clean, reusable, and DRY (Don’t Repeat Yourself).
🧠 How Decorators Work
- A decorator is a higher-order function: it takes a function as input and returns a modified version of that function.
- It wraps the target function, allowing extra logic to run before or after it.
- This is a form of metaprogramming — writing code that manipulates other code.
🛠️ Common Use Cases
- 🔐 Authentication/Authorization – check user permissions before executing a function.
- 🧾 Logging – record function calls and arguments.
- 📦 Caching – store and reuse expensive computations.
- ✅ Validation – verify arguments before proceeding.
- ⏰ Scheduling – delay or repeat function calls.
- 📊 Profiling – measure performance or count executions.
💻 Example: A Basic Decorator
from functools import wraps def my_decorator(func): @wraps(func) # Preserve metadata like function name and docstring def wrapper(*args, **kwargs): print("🔍 Before the function is called") result = func(*args, **kwargs) print("✅ After the function is called") return result return wrapper @my_decorator def greet(name): print(f"Hello, {name}!") greet("Alice")
Output:
🔍 Before the function is called Hello, Alice! ✅ After the function is called
📌 Decorator Syntax Shortcut
@my_decorator def say_hello(): print("Hello!")
is equivalent to:
def say_hello(): print("Hello!") say_hello = my_decorator(say_hello)
🔖 Why functools.wraps
Matters
Without @wraps(func)
, the decorated function loses its identity (name, docstring, etc.). wraps()
ensures tools like introspection, documentation, or debugging work correctly.
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
A decorator in Python is like a “wrapper” you can put around a function to add extra behavior — without modifying the original function’s code.
For example, you might want to log every time a function is called, or restrict access to a view in Django. Instead of adding that logic to every function, you create a decorator and reuse it — just by writing @your_decorator
above the function.
It helps keep your code clean, modular, and easy to maintain.
What is list comprehension? Give an example.
💡 Answer:
In Python, a list comprehension is a concise and expressive way to create lists. It lets you generate a list in a single line by combining a for
loop and optional if
condition inside square brackets.
⚙️ Technical Explanation
- Syntax:
[expression for item in iterable if condition]
- It replaces verbose loops with a cleaner and more Pythonic syntax.
- Can include transformations and filters.
🧪 Example 1: Generating Squares
squares = [x**2 for x in range(10)] print(squares) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
🧪 Example 2: Filtering Even Numbers
evens = [x for x in range(20) if x % 2 == 0] print(evens) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
🔁 Equivalent with Regular Loop (for comparison)
squares = [] for x in range(10): squares.append(x**2)
Same output, but more verbose.
🧠 Use Cases
- Data cleaning (e.g., strip/transform strings)
- Filtering or mapping from datasets
- Efficient inline list building
📌 Syntax Notes
- Nested list comprehensions are supported (use with care).
- You can also use comprehensions for dicts and sets:
{x: x**2 for x in range(5)} # dictionary {x**2 for x in range(5)} # set
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
A list comprehension is just a super compact way to build a list in Python. Instead of writing a full for
loop and calling append()
, you can do everything in one line.
For example, if I want a list of squares from 0 to 9, I just write:[x**2 for x in range(10)]
— and that’s it!
It makes your code cleaner, faster to write, and easier to read — especially when working with large data sets or doing simple transformations.
Explain dictionary comprehension.
💡 Answer:
In Python, dictionary comprehension is a concise method to create dictionaries using a single line of code. It mirrors list comprehension but constructs key-value pairs instead.
⚙️ Technical Explanation
-
Syntax:
{key: value for item in iterable}
- You can add conditions to include or transform specific elements:
{k: v for k, v in iterable if condition}
- Typically uses:
- A list of tuples
-
zip()
combinations - Existing dictionaries
🧪 Example 1: Zip Two Lists into a Dict
cities = ['New York', 'Los Angeles', 'Denver', 'Phoenix'] time_zones = ['-0500', '-0800', '-0700', '-0700'] city_time_zones = {city: tz for city, tz in zip(cities, time_zones)} print(city_time_zones)
Output:
{'New York': '-0500', 'Los Angeles': '-0800', 'Denver': '-0700', 'Phoenix': '-0700'}
🧪 Example 2: Filter Values
nums = [1, 2, 3, 4, 5] squared_evens = {x: x**2 for x in nums if x % 2 == 0} print(squared_evens)
Output:
{2: 4, 4: 16}
🧠 Use Cases
- 💼 Data transformation – Modify an existing dictionary or pairwise data.
- 🧪 Filtering – Build a dictionary based on conditions.
- 🔁 Swapping keys and values – Reverse mappings:
original = {'a': 1, 'b': 2} inverted = {v: k for k, v in original.items()}
📌 Considerations
- ✅ Keys must be unique – Later entries will overwrite earlier ones if duplicates exist.
- ⚠️ Avoid overcomplicating logic inside comprehensions – maintain readability.
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
Dictionary comprehension is just like list comprehension, but instead of creating a list, you’re creating a dictionary — with key-value pairs.
Let’s say I have two lists: one with cities and another with time zones. I can combine them into a dictionary in one line using:
{city: tz for city, tz in zip(cities, time_zones)}
It’s especially handy for transforming data, filtering items, or reversing a dictionary. It keeps your code short, readable, and expressive — perfect for quick tasks where defining a full for
loop would feel unnecessary.
What are generators in Python, and how do you use them?
Using a regular list (not memory-efficient for large n)
A generator in Python is basically a special type of function that lets you produce values one at a time, instead of generating and returning a full list all at once.
Instead of using return, you use the yield keyword. The cool thing is, every time the generator yields a value, it pauses and saves its state. When you call it again, it picks up right where it left off. So you’re not building a whole list in memory — just streaming one item at a time.
This is super useful when you’re working with big datasets, or maybe you’re reading lines from a large file, or looping through an infinite sequence like numbers going on forever. It’s very memory-efficient because it doesn’t load everything at once.
What is multiprocessing
and multi-threading
in Python, and how do they differ?
💡 Answer:
Both are techniques for achieving concurrency, but they handle parallelism very differently due to Python’s architecture — especially the Global Interpreter Lock (GIL).
🧵 1. Multi-Threading in Python
🧠 What It Is:
Multi-threading allows you to run multiple threads (lightweight units of a process) concurrently within the same process memory space.
🔒 Limitation — The GIL:
A race condition happens when two or more threads (or processes) access shared data at the same time, and the final result depends on who gets there first — like a “race.” This can lead to unpredictable, incorrect, or inconsistent behavior in your program.
Python’s GIL ensures that only one thread runs Python bytecode at a time, even on multi-core CPUs.
#### ✅ Best For:
- I/O-bound tasks: web scraping, file I/O, socket connections.
- Situations where threads spend time waiting (not computing).
🧪 Code Example:
import threading import time def do_task(name): print(f"Starting {name}") time.sleep(2) print(f"Finished {name}") t1 = threading.Thread(target=do_task, args=("Task 1",)) t2 = threading.Thread(target=do_task, args=("Task 2",)) t1.start() t2.start() t1.join() t2.join()
Output: The two tasks appear to run in parallel — but only one executes Python code at a time due to the GIL.
⚙️ 2. Multiprocessing in Python
🧠 What It Is:
Multiprocessing creates separate processes, each with its own Python interpreter and memory space, allowing true parallelism — even for CPU-bound tasks.
💪 No GIL Restriction:
Each process runs independently, bypassing the GIL, making multiprocessing suitable for heavy computations.
✅ Best For:
- CPU-bound tasks: image processing, data crunching, mathematical simulations.
- When you want parallel execution on multiple cores.
🧪 Code Example:
from multiprocessing import Process import time def compute_heavy(name): print(f"Starting {name}") time.sleep(2) print(f"Finished {name}") p1 = Process(target=compute_heavy, args=("Job 1",)) p2 = Process(target=compute_heavy, args=("Job 2",)) p1.start() p2.start() p1.join() p2.join()
Output: Both jobs run in true parallel across CPU cores.
What is the Global Interpreter Lock (GIL) in Python?
💡 Answer:
The Global Interpreter Lock (GIL) is a mutex (or a lock) used in the CPython implementation of Python. It ensures that only one thread executes Python bytecode at a time, even on multi-core processors. While it simplifies memory management internally, it also restricts true parallel execution of Python threads — particularly in CPU-bound tasks.
🔍 GIL’s Role in Multi-Threading
⚙️ CPU-bound Tasks
- The GIL becomes a bottleneck when multiple threads perform CPU-intensive work.
- Python threads cannot fully utilize multiple cores simultaneously because the GIL only lets one thread execute Python code at a time.
- Libraries like NumPy and Pandas often work around the GIL using optimized C extensions.
🧾 I/O-bound Tasks
- For tasks like reading files or making HTTP requests, the GIL is not a major issue.
- While one thread waits on I/O, the GIL can switch to another thread, improving concurrency.
⏱️ Blocking vs. Non-blocking I/O
-
Non-blocking I/O (e.g.,
select
,poll
) allows single-threaded programs to handle multiple I/O tasks, reducing GIL-related bottlenecks.
🖥️ Multi-Core vs. Single-Core
- On multi-core machines, GIL limits true parallelism, reducing performance gains.
- Python 3.8+ is exploring subinterpreters, which may have separate GILs, potentially solving this issue in the future.
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
“It’s a lock that prevents multiple Python threads from running Python code at the same time — even on multi-core machines.”
So if you’re doing CPU-heavy work and trying to use threads, the GIL will slow you down because it doesn’t allow threads to run in parallel. But for I/O-bound tasks like web scraping or downloading files, the GIL isn’t a big problem — threads just take turns while others wait on I/O.
In short, GIL makes Python easier to manage internally, but it limits performance for parallel computing. That’s why we often use multiprocessing instead of multithreading for heavy tasks in Python.
Let me know if you’d like this illustrated with a diagram or compared with how Java or Go handles concurrency.
What are classes in Python ?
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
A class in Python is like a blueprint for creating objects. It defines what properties and behaviors an object should have. For example, if I have a class called Car
, it might have attributes like brand
and model_year
, and methods like honk()
.
We use classes when we want to model real-world things like users, vehicles, or bank accounts. They also help keep the code clean and manageable by bundling data and functions together logically.
💻 Updated Simple Class Example
class Car: def \_\_init\_\_(self, brand, model_year): self.brand = brand self.model_year = model_year def honk(self): return f"{self.brand} goes Beep!" my_car = Car("Toyota", 2022) print(my_car.honk())
What are instance methods, class methods, and static methods ?
💡 Answer:
In Python, methods inside a class can behave differently based on how they are defined. The three main types are instance methods, class methods, and static methods — and each serves a distinct purpose.
👤 Instance Methods
- Definition: Operate on a specific object (instance) of a class and have access to instance attributes.
-
Keyword: Always take
self
as the first argument. - Use Case: Used when you need to access or modify the object’s data.
class Car: def \_\_init\_\_(self, speed): self.speed = speed def double_speed(self): self.speed *= 2 # Accessing instance attribute
✅ Call like: my_car.double_speed()
🏛️ Class Methods
- Definition: Operate on the class itself, not individual objects.
-
Keyword: Use
cls
instead ofself
. - Use Case: Modify or access class-level data shared by all instances.
class Car: cars_created = 0 @classmethod def increment_count(cls): cls.cars_created += 1
✅ Call like: Car.increment_count()
🧰 Static Methods
-
Definition: Do not take
self
orcls
. They behave like regular functions but belong to the class’s namespace. - Use Case: Utility/helper methods that don’t access or change class or instance state.
class Validator: @staticmethod def is_valid_email(email): return "@" in email
✅ Call like: Validator.is_valid_email("abc@example.com")
🎙️ Interview-Friendly Casual Explanation (Diễn giải để nói trôi chảy):
So, in Python, there are three types of methods:
- Instance methods are the most common — they work with object data, and you need
self
to access or modify the object’s attributes. - Class methods use
cls
and are useful when you want to do something at the class level — like tracking how many instances have been created. - Static methods don’t care about the class or the object — they’re just plain functions tucked into a class for organization.
Knowing when to use each helps keep your code modular, reusable, and clean.
What is super() in Python
🎙️ Interview-Friendly Casual Explanation:
“In Python, I use super()
when I want a child class to reuse or extend behavior from its parent class. Instead of hardcoding the parent class name, I call super().method()
What is iterator in Python ?
💡 Answer:
An iterator in Python is an object that enables iteration over a sequence (like lists, tuples, strings) or any object that follows the iterator protocol. This protocol requires two methods:
-
\_\_iter\_\_()
— Returns the iterator object itself. -
\_\_next\_\_()
— Returns the next item from the sequence and raisesStopIteration
when done.
🔧 Technical Explanation
In Python:
- Any object with both
\_\_iter\_\_()
and\_\_next\_\_()
methods is an iterator. - The
iter()
function creates an iterator from an iterable. - The
next()
function retrieves the next element from the iterator.
🧪 Code Example: Basic Iterator
my_list = [1, 2, 3] it = iter(my_list) # Get iterator print(next(it)) # 1 print(next(it)) # 2 print(next(it)) # 3 print(next(it)) # Raises StopIteration
🔁 Creating a Custom Iterator Class
class Countdown: def \_\_init\_\_(self, start): self.current = start def \_\_iter\_\_(self): return self def \_\_next\_\_(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1 for number in Countdown(3): print(number)
🖨️ Output:
3 2 1
⚡ Use Cases
- Efficient looping over large datasets.
- Custom sequences that aren’t stored in memory (like streamed data).
- Lazy evaluation — only computing what’s needed next.
🎙️ Interview-Friendly Casual Explanation:
“In Python, an iterator is just an object that lets you loop through a sequence one item at a time using next()
. Under the hood, loops like for
use iterators. You can even build your own iterator class by defining \_\_iter\_\_()
and \_\_next\_\_()
— pretty useful when you’re working with custom or huge datasets that you don’t want to load into memory all at once.”
Can you explain what Object-Oriented Programming is in Python and describe its four main principles with examples?
💡 Answer:
🧱 What is OOP (Object-Oriented Programming) in Python?
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects”, which bundle data (attributes) and functions (methods) together. Python fully supports OOP, making it easier to model real-world entities, improve code reuse, and enhance maintainability.
🔑 The 4 Pillars of OOP
- 🔐 Encapsulation
- Definition: Hiding internal state and requiring all interaction to be performed through methods.
- Why it matters: Prevents external interference and protects object integrity.
class BankAccount: def \_\_init\_\_(self, balance): self.\_\_balance = balance # Private variable def deposit(self, amount): self.\_\_balance += amount def get_balance(self): return self.\_\_balance
- ☑️
\_\_balance
is encapsulated; external code can’t directly access or modify it.
- 🧬 Inheritance
- Definition: A mechanism where one class (child) inherits the attributes and methods from another (parent).
- Why it matters: Promotes code reuse and logical hierarchy.
class Animal: def speak(self): print("Animal sound") class Dog(Animal): def speak(self): print("Woof!")
- 🐶
Dog
inheritsspeak()
fromAnimal
but can override it.
- 🎭 Polymorphism
- Definition: The ability of different classes to be treated as instances of the same interface, often by overriding methods.
- Why it matters: Enables flexible and extensible code.
class Bird: def speak(self): print("Tweet!") class Cat: def speak(self): print("Meow!") def animal_sound(animal): animal.speak() animal_sound(Bird()) # Tweet! animal_sound(Cat()) # Meow!
- 🎯 One function
animal_sound()
can work with many object types.
- 🧩 Abstraction
- Definition: Hiding complex implementation details and showing only essential features.
- Why it matters: Simplifies the interface and increases clarity.
from abc import ABC, abstractmethod class Vehicle(ABC): @abstractmethod def start_engine(self): pass class Car(Vehicle): def start_engine(self): print("Engine started.")
- 🛻 The user only needs to know how to start the engine, not how it works internally.
🎙️ Interview-Friendly Casual Explanation:
“OOP is like modeling your code after the real world. You group data and behavior into objects. With encapsulation, you protect internal details. Inheritance lets you reuse code by extending parent classes. Polymorphism gives you flexibility by treating different classes the same. Abstraction keeps things clean by hiding the complex stuff and only showing what’s needed.”
Design pattern ?
💡 Question:
“What are some common design patterns in software engineering, and how do Singleton, Factory Method, Abstract Factory, Adapter, and Decorator patterns differ in their usage and purpose? Provide examples in Python.”
💡 Answer:
Design patterns are reusable solutions to common problems in software design. Here’s a breakdown of 5 popular patterns along with their use cases and Python examples:
🔹 1. Singleton Pattern (Creational)
🧠 Purpose:
Ensure a class has only one instance and provide a global access point to it.
📦 Use Case:
Database connection manager, logging systems, configuration managers.
💡 Python Example:
class Singleton: _instance = None def \_\_new\_\_(cls): if cls._instance is None: cls._instance = super().\_\_new\_\_(cls) return cls._instance db1 = Singleton() db2 = Singleton() print(db1 is db2) # Output: True (only one instance)
🔹💡 Answer: Easy-to-Understand + Technical Explanation of Factory Pattern (in English)
🏭 What is the Factory Pattern?
> Factory Pattern is a design pattern that lets you create objects without specifying the exact class of the object being created.
🧠 Simple but Technical Definition:
- You delegate the object creation logic to a separate class (called a “Factory”).
- Instead of calling
new SomeClass()
directly, you call a factory method likecreate_object("type")
, and it gives you the correct object. - This pattern is useful when:
- You have many subclasses or types.
- You want to hide the creation logic from the client.
- You want to make your code easier to extend later.
✅ Quick Real-Life Analogy:
> Think of a coffee shop (the Factory). You just say “I want a Latte” or “Espresso”, and the barista (factory) knows which recipe (class) to use and gives you the correct coffee (object).
You don’t need to know how it’s made — just ask, and get the right one.
🔧 Simple Python Example (Technical + Easy):
```python
class Dog:
def speak(self):
return “Woof!”
class Cat:
def speak(self):
return “Meow!”
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == “dog”:
return Dog()
elif animal_type == “cat”:
return Cat()
else:
raise ValueError(“Unknown animal type”)
Client code
animal = AnimalFactory.create_animal(“dog”)
print(animal.speak()) # Outputs: Woof!
~~~
🧩 When Should You Use Factory Pattern?
- You don’t want to write
if/else
everywhere to decide which class to create. - You need to centralize object creation logic.
- You want to support plug-and-play behavior (e.g., new payment methods, new file formats, etc.).
- You follow Open/Closed Principle (open to extension, closed to modification).
🎙️ Super Simple Explanation for Interview:
> “The Factory Pattern is like a smart object creator. Instead of me writing new Dog()
or new Cat()
everywhere, I just ask the factory: ‘Hey, give me an animal of type X’, and it gives me the correct one. This way, I don’t worry about the details, and if I add a new type tomorrow, I only update the factory, not the whole app.”
💡 Python Example:
class Animal: def speak(self): pass class Dog(Animal): def speak(self): return "Woof!" class Cat(Animal): def speak(self): return "Meow!" def animal_factory(animal_type): if animal_type == "dog": return Dog() elif animal_type == "cat": return Cat() pet = animal_factory("dog") print(pet.speak()) # Output: Woof!
🔹 5. Decorator Pattern (Structural)
🧠 Purpose:
Adds new functionality to an object dynamically without altering its structure.
📦 Use Case:
Used in logging, access control, or extending behavior.
💡 Python Example:
def bold_decorator(func): def wrapper(): return "<b>" + func() + "</b>" return wrapper @bold_decorator def greet(): return "Hello" print(greet()) # Output: <b>Hello</b>
SOLID ?
💡 Answer: What is SOLID in OOP? (With Simple English & Easy Examples)
SOLID is a set of 5 design principles in Object-Oriented Programming (OOP) that help you write clean, maintainable, and scalable code. Each letter stands for a principle:
🅢 Single Responsibility Principle (SRP)
👉 One class should do only one thing.
- ✅ Simple Explanation:
A class should have only one reason to change — it should have one job only. - 🧸 Example:```
class Invoice:
def calculate_total(self):
passclass InvoicePrinter:
def print_invoice(self):
pass
```→Invoice
handles calculations,InvoicePrinter
handles printing. No mixing!
🅞 Open/Closed Principle (OCP)
👉 Open for extension, closed for modification.
- ✅ Simple Explanation:
You can add new features by writing new code, but you should not change existing code. - 🧸 Example:```
class Shape:
def area(self): passclass Circle(Shape):
def area(self): return 3.14 * r * r
```→ Adding new shapes? Just make a new class — don’t touch the old ones.
🅛 Liskov Substitution Principle (LSP)
👉 Child classes must be usable wherever parent classes are used.
- ✅ Simple Explanation:
IfDog
is a subclass ofAnimal
, you should be able to useDog
instead ofAnimal
without errors. - ❌ Bad Example:```
class Bird:
def fly(self): passclass Penguin(Bird):
def fly(self): raise Exception(“Can’t fly”)
```→ Penguins can’t fly, so they shouldn’t inherit from Bird if Bird expectsfly()
.
🅘 Interface Segregation Principle (ISP)
👉 Don’t force classes to implement things they don’t need.
- ✅ Simple Explanation:
Make smaller, more specific interfaces so classes only implement what they actually use. - 🧸 Example:
Instead of:class Worker: def work(self): pass def eat(self): pass
Split it:```
class Workable:
def work(self): passclass Eatable:
def eat(self): pass
```
🅓 Dependency Inversion Principle (DIP)
👉 Depend on abstractions, not concrete classes.
- ✅ Simple Explanation:
Your classes should rely on interfaces, not on specific implementations. - 🧸 Example:```
class EmailSender:
def send(self, msg): print(“Email sent!”)class Notifier:
def __init__(self, sender): # sender is passed in
self.sender = senderdef notify(self): self.sender.send("Hi!") ```
→ We inject the dependency instead of creating it inside.
🎙️ Interview-Friendly Casual Explanation:
- “SOLID helps keep your code clean, just like good habits keep your room clean. Each principle prevents future messes. For example, SRP makes sure your classes don’t become a big ‘God object,’ and OCP lets you add features without breaking things. It’s like building Lego — well-separated, easy to snap on new pieces without wrecking the old ones.”