Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

🎛️ What is Abstraction?

When you use a TV remote, you just press buttons:
  • 📺 Power button to turn on/off
  • 🔊 Volume buttons to adjust sound
  • 📻 Channel buttons to switch channels
You don’t need to know:
  • How radio signals work
  • How the display creates images
  • How the speakers produce sound
The remote abstracts away all the complexity!
Simple Definition: Abstraction = Showing only the ESSENTIAL features and hiding the implementation details
The restaurant menu analogy: When you order at a restaurant, the menu is an abstraction. It tells you what you can get (names, descriptions, prices) but hides how it is made (recipes, cooking techniques, supplier logistics). You do not need to know that the pasta sauce simmers for four hours or that the tomatoes come from a specific farm. The menu is the right level of detail for a customer. Abstraction in code works the same way — you define what an operation does for the caller while hiding how it accomplishes it internally.

🚗 Real-World Examples

🚗 Driving a Car

What you see: Steering wheel, pedals, buttonsWhat’s hidden: Engine mechanics, fuel injection, transmission

☕ Coffee Machine

What you see: One button to make coffeeWhat’s hidden: Water heating, pressure, grinding

📱 Smartphone

What you see: Touch screen, appsWhat’s hidden: CPU, memory, network protocols

🏧 ATM Machine

What you see: Screen, keypad, card slotWhat’s hidden: Bank servers, encryption, networking

🐍 Abstraction in Python

Python uses Abstract Base Classes (ABC) to create abstractions:
from abc import ABC, abstractmethod

# 🎨 Abstract class - a TEMPLATE that says "what" but not "how"
class Shape(ABC):
    
    @abstractmethod
    def area(self):
        """Calculate and return the area"""
        pass  # No implementation! Just a promise.
    
    @abstractmethod
    def perimeter(self):
        """Calculate and return the perimeter"""
        pass
    
    # Non-abstract method - has implementation
    def describe(self):
        print(f"I am a shape with area: {self.area()}")

# ❌ This will ERROR - can't create abstract class directly
# shape = Shape()  # TypeError!

# ✅ Concrete classes MUST implement all abstract methods
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):  # ✅ Implemented!
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):  # ✅ Implemented!
        return 2 * 3.14159 * self.radius

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):  # ✅ Implemented!
        return self.width * self.height
    
    def perimeter(self):  # ✅ Implemented!
        return 2 * (self.width + self.height)

# Now we can use them!
circle = Circle(5)
rect = Rectangle(4, 6)

print(f"Circle area: {circle.area()}")          # 78.54
print(f"Rectangle perimeter: {rect.perimeter()}")  # 20
circle.describe()  # Uses the non-abstract method

💳 Practical Example: Payment Gateway

Let’s build a payment system where the complex payment logic is hidden:
from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    """Abstract payment gateway - defines WHAT operations are available"""
    
    @abstractmethod
    def connect(self):
        """Connect to the payment service"""
        pass
    
    @abstractmethod
    def authenticate(self, credentials):
        """Authenticate with the service"""
        pass
    
    @abstractmethod
    def process_payment(self, amount, card_details):
        """Process the actual payment"""
        pass
    
    @abstractmethod
    def disconnect(self):
        """Clean up connection"""
        pass
    
    # Template method - uses abstract methods
    def make_payment(self, amount, card_details, credentials):
        """High-level payment flow - same for all gateways"""
        print(f"\n💳 Processing ${amount:.2f} payment...")
        
        self.connect()
        if self.authenticate(credentials):
            result = self.process_payment(amount, card_details)
            self.disconnect()
            return result
        
        self.disconnect()
        return False

# Concrete implementation for Stripe
class StripeGateway(PaymentGateway):
    def connect(self):
        print("   🔌 Connecting to Stripe API...")
    
    def authenticate(self, credentials):
        print(f"   🔐 Authenticating with API key: {credentials[:8]}...")
        return True  # Simplified
    
    def process_payment(self, amount, card_details):
        print(f"   💰 Stripe processing ${amount:.2f}...")
        print(f"   ✅ Payment successful! Stripe ID: ch_123abc")
        return True
    
    def disconnect(self):
        print("   👋 Disconnected from Stripe")

# Concrete implementation for PayPal
class PayPalGateway(PaymentGateway):
    def connect(self):
        print("   🔌 Connecting to PayPal sandbox...")
    
    def authenticate(self, credentials):
        print(f"   🔐 OAuth2 authentication...")
        return True
    
    def process_payment(self, amount, card_details):
        print(f"   💰 PayPal processing ${amount:.2f}...")
        print(f"   ✅ Payment successful! PayPal ID: PAY-xyz789")
        return True
    
    def disconnect(self):
        print("   👋 PayPal session closed")

# Concrete implementation for Square
class SquareGateway(PaymentGateway):
    def connect(self):
        print("   🔌 Initializing Square terminal...")
    
    def authenticate(self, credentials):
        print(f"   🔐 Square access token verified")
        return True
    
    def process_payment(self, amount, card_details):
        print(f"   💰 Square processing ${amount:.2f}...")
        print(f"   ✅ Payment successful! Square ID: sq_456def")
        return True
    
    def disconnect(self):
        print("   👋 Square terminal disconnected")

# DESIGN REASONING: The caller only knows about make_payment().
# Which payment provider is used, how authentication works, what
# protocol is spoken -- all of that is hidden behind the abstraction.
# This is what lets you swap Stripe for PayPal without changing
# a single line of calling code.

print("=" * 50)
print("Testing different payment gateways")
print("=" * 50)

stripe = StripeGateway()
stripe.make_payment(99.99, {"card": "****1234"}, "sk_test_12345")

paypal = PayPalGateway()
paypal.make_payment(49.99, {"card": "****5678"}, "client_id_xyz")

square = SquareGateway()
square.make_payment(29.99, {"card": "****9999"}, "sq_access_token")

🎮 Example: Game Engine Abstraction

from abc import ABC, abstractmethod

class GameEngine(ABC):
    """Abstract game engine - hide rendering complexity"""
    
    @abstractmethod
    def initialize(self):
        pass
    
    @abstractmethod
    def render_sprite(self, sprite, x, y):
        pass
    
    @abstractmethod
    def play_sound(self, sound_file):
        pass
    
    @abstractmethod
    def check_collision(self, obj1, obj2):
        pass
    
    # Template for game loop
    def run_frame(self, game_objects):
        for obj in game_objects:
            self.render_sprite(obj.sprite, obj.x, obj.y)

class Unity2DEngine(GameEngine):
    def initialize(self):
        print("🎮 Unity 2D initializing...")
        print("   Loading shaders, setting up OpenGL context...")
    
    def render_sprite(self, sprite, x, y):
        print(f"   [Unity] Drawing '{sprite}' at ({x}, {y})")
    
    def play_sound(self, sound_file):
        print(f"   [Unity] Playing '{sound_file}' via FMOD")
    
    def check_collision(self, obj1, obj2):
        print(f"   [Unity] Box2D collision check...")
        return False

class PygameEngine(GameEngine):
    def initialize(self):
        print("🎮 Pygame initializing...")
        print("   pygame.init() called, display created...")
    
    def render_sprite(self, sprite, x, y):
        print(f"   [Pygame] blit '{sprite}' at ({x}, {y})")
    
    def play_sound(self, sound_file):
        print(f"   [Pygame] mixer.Sound('{sound_file}').play()")
    
    def check_collision(self, obj1, obj2):
        print(f"   [Pygame] rect.colliderect() check...")
        return False

# Game developer doesn't need to know engine internals!
class GameObject:
    def __init__(self, name, sprite, x, y):
        self.name = name
        self.sprite = sprite
        self.x = x
        self.y = y

# Same game code works with different engines
def run_game(engine):
    print("\n" + "=" * 50)
    engine.initialize()
    
    objects = [
        GameObject("Player", "hero.png", 100, 200),
        GameObject("Enemy", "monster.png", 300, 200),
    ]
    
    engine.run_frame(objects)
    engine.play_sound("jump.wav")

print("Testing with Unity:")
run_game(Unity2DEngine())

print("\nTesting with Pygame:")
run_game(PygameEngine())

Why Abstraction Matters in Production

In large systems, abstraction is what allows teams to work independently. At a company like Uber, the Ride Matching team defines an abstract PaymentService interface. The Payments team implements it with Stripe, Braintree, or whatever provider they choose. The Ride Matching team never needs to know (or care) which payment provider processes the charge. If the Payments team migrates from Stripe to Adyen, the Ride Matching code does not change at all. A senior engineer would say: “Good abstraction is about choosing the right level of detail to expose. Too little abstraction and every caller is coupled to your implementation. Too much abstraction and you end up with layers of indirection that nobody can debug. The sweet spot is an interface that maps to a clear business concept — like PaymentGateway, NotificationChannel, or StorageBackend.”

Abstraction vs Encapsulation

People often confuse these two! Here’s the difference:
AspectAbstractionEncapsulation
FocusHiding COMPLEXITYHiding DATA
GoalShow only essential featuresProtect data integrity
HowAbstract classes, interfacesPrivate attributes, getters/setters
LevelDesign levelImplementation level
ExampleTV remote (hide how TV works)Bank account (hide balance)
from abc import ABC, abstractmethod

# ABSTRACTION - hiding complexity of HOW something works
class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self): pass
    
    @abstractmethod
    def query(self, sql): pass

# ENCAPSULATION - hiding and protecting DATA
class User:
    def __init__(self, name, password):
        self.name = name
        self.__password = password  # 🔒 Hidden data
    
    def check_password(self, attempt):
        return attempt == self.__password  # Controlled access

🏗️ Building Layers of Abstraction

Good software has layers - each layer hides complexity from the one above:
┌─────────────────────────────────────────────┐
│              Your Application               │  ← You write this
│         (Just calls simple methods)         │
├─────────────────────────────────────────────┤
│              Framework Layer                │  ← Abstracts complexity
│      (Django, Flask, FastAPI, etc.)         │
├─────────────────────────────────────────────┤
│              Library Layer                  │  ← More abstractions
│    (requests, SQLAlchemy, Pillow, etc.)     │
├─────────────────────────────────────────────┤
│            Language Runtime                 │  ← Python abstracts C
│               (Python)                      │
├─────────────────────────────────────────────┤
│           Operating System                  │  ← OS abstracts hardware
│        (Windows, Linux, macOS)              │
├─────────────────────────────────────────────┤
│               Hardware                      │  ← The complex reality
│     (CPU, Memory, Disk, Network)            │
└─────────────────────────────────────────────┘

Example: File Operations

# What YOU write (simple):
with open("data.txt", "w") as f:
    f.write("Hello!")

# What Python ACTUALLY does (complex):
# 1. Ask OS to create file descriptor
# 2. Allocate memory buffer
# 3. Convert string to bytes
# 4. Handle different file systems
# 5. Manage disk sectors
# 6. Handle concurrent access
# 7. Flush buffers to disk
# 8. Release file descriptor
# 9. Free memory

# All that complexity is ABSTRACTED away! ✨

🔌 Interfaces in Python

While Python doesn’t have formal interfaces like Java, we use Abstract Base Classes (ABC) the same way:
from abc import ABC, abstractmethod

# This is essentially an INTERFACE
class Printable(ABC):
    @abstractmethod
    def to_string(self):
        pass
    
    @abstractmethod
    def print_formatted(self):
        pass

class Invoice(Printable):
    def __init__(self, number, amount):
        self.number = number
        self.amount = amount
    
    def to_string(self):
        return f"Invoice #{self.number}: ${self.amount}"
    
    def print_formatted(self):
        print("╔════════════════════════════╗")
        print(f"║  Invoice: {self.number:>15} ║")
        print(f"║  Amount:  ${self.amount:>13.2f} ║")
        print("╚════════════════════════════╝")

class Report(Printable):
    def __init__(self, title, data):
        self.title = title
        self.data = data
    
    def to_string(self):
        return f"Report: {self.title}"
    
    def print_formatted(self):
        print(f"\n📊 {self.title}")
        print("-" * 30)
        for key, value in self.data.items():
            print(f"  {key}: {value}")

# Any Printable can be printed!
def print_document(doc: Printable):
    print(doc.to_string())
    doc.print_formatted()

invoice = Invoice("INV-001", 1250.00)
report = Report("Sales Summary", {"Q1": "$10k", "Q2": "$15k", "Q3": "$12k"})

print_document(invoice)
print_document(report)

✅ Benefits of Abstraction

🧩 Simplicity

Complex systems become easy to understand and use

🔄 Flexibility

Easy to swap implementations without changing code

🛡️ Security

Implementation details are hidden from misuse

📦 Modularity

Components can be developed independently

🧪 Practice Exercise

Create an abstract NotificationService that defines:
  • send(recipient, message) - Send a notification
  • format_message(message) - Format the message
  • validate_recipient(recipient) - Check if recipient is valid
Then implement:
  1. EmailService - Sends emails
  2. SMSService - Sends text messages
  3. SlackService - Sends Slack messages
from abc import ABC, abstractmethod

class NotificationService(ABC):
    @abstractmethod
    def send(self, recipient, message):
        pass
    
    @abstractmethod
    def format_message(self, message):
        pass
    
    @abstractmethod
    def validate_recipient(self, recipient):
        pass
    
    # Template method
    def notify(self, recipient, message):
        if self.validate_recipient(recipient):
            formatted = self.format_message(message)
            return self.send(recipient, formatted)
        return False

class EmailService(NotificationService):
    # TODO: Implement all methods
    pass

# TODO: Add SMSService and SlackService

# Test
def send_notification(service, recipient, msg):
    service.notify(recipient, msg)

services = [
    EmailService(),
    SMSService(),
    SlackService()
]

for service in services:
    send_notification(service, "user@example.com", "Hello!")

📝 Quick Summary

ConceptDescription
AbstractionHide complexity, show only essentials
Abstract ClassTemplate class with some methods undefined
Abstract MethodMethod without implementation (must be overridden)
Concrete ClassClass that implements all abstract methods
InterfacePure abstract class (all methods abstract)

🎉 OOP Complete!

You’ve now learned all four pillars of OOP:

📦 Encapsulation

Bundling data and methods, controlling access

👨‍👩‍👧 Inheritance

Creating hierarchies, reusing code

🦎 Polymorphism

Same interface, different behaviors

🎛️ Abstraction

Hiding complexity, showing essentials

Interview Insight

Abstraction vs Encapsulation — the classic interview question: Interviewers love asking “what is the difference between abstraction and encapsulation?” Here is the crisp answer that impresses: Abstraction is a design-level concept about hiding complexity (deciding what to expose), while encapsulation is an implementation-level mechanism for protecting data (using access modifiers). Abstraction answers “what should the caller see?” Encapsulation answers “how do we enforce that boundary?” They work together: abstraction defines the interface, and encapsulation enforces it. A strong follow-up: “In practice, I define abstract classes to set the contract (abstraction), and use private attributes within concrete classes to protect state (encapsulation).”

Interview Deep-Dive

Strong Answer:
  • This is the Template Method pattern. The abstract class defines the algorithm skeleton in make_payment() — connect, authenticate, process, disconnect — but delegates the specific steps to abstract methods that subclasses must implement. The high-level flow is fixed (and tested once); the implementation details vary per provider.
  • The concrete method make_payment() is the “template.” It calls the abstract methods in a specific order, handling the shared logic (error checking, sequencing) so subclasses do not need to duplicate it. StripeGateway, PayPalGateway, and SquareGateway each implement the four abstract methods differently, but the orchestration is identical.
  • This directly applies the DRY principle (Don’t Repeat Yourself) — without the template method, every gateway would independently implement the connect-authenticate-process-disconnect sequence, leading to subtle bugs when one forgets a step.
  • In an interview, I would name this pattern explicitly: “I am using Template Method here because the algorithm is the same but the steps vary by provider.”
Follow-up: What is the difference between Template Method and Strategy pattern? Both delegate to subclasses.Template Method uses inheritance: the algorithm is in the base class, and subclasses override specific steps. Strategy uses composition: the algorithm is in a separate object that is injected. Template Method is appropriate when the algorithm structure is fixed and only the steps vary. Strategy is appropriate when the entire algorithm might be swapped. If you might want to change the payment flow itself (not just the provider), Strategy is better.
Strong Answer:
  • Abstraction is a design-level concept: what should the caller see? It hides complexity by defining contracts (interfaces, abstract classes) that separate “what” from “how.” The PaymentGateway ABC says “you can process payments and issue refunds” without revealing whether the implementation uses REST APIs, gRPC, or carrier pigeons.
  • Encapsulation is an implementation-level mechanism: how do we enforce the boundary? It hides data by using access modifiers (private fields, properties) to protect internal state. The StripeGateway class keeps its api_key private so external code cannot read or modify it.
  • They work together: abstraction defines the interface, encapsulation enforces it. A PaymentGateway interface without encapsulation in the implementations would allow external code to directly access api_key or bypass the connect() step. Encapsulation without abstraction would protect the data but not provide a clean contract for consumers.
  • Real example: when you use the requests library in Python, you call requests.get(url). That is abstraction — you do not know how it manages HTTP connections, TLS, or DNS resolution. Internally, requests uses encapsulation to protect its connection pool, session state, and retry logic from external modification.
Follow-up: Can you have abstraction without inheritance?Yes. Python’s duck typing provides abstraction without inheritance: any object with a send() method can serve as a notification channel, even without inheriting from NotificationChannel. The Protocol class from typing formalizes this as structural subtyping. Go’s interfaces work the same way — implicit satisfaction without declaration. The abstraction comes from the contract (what methods exist), not from the inheritance relationship.
Strong Answer:
  • I would create a contract test suite — a set of tests that any GameEngine implementation must pass. The test class is parameterized: it takes a GameEngine instance as input and runs the same assertions against it.
  • For example: test_initialize_succeeds() verifies that initialize() does not throw. test_render_sprite_handles_offscreen() verifies that render_sprite() with coordinates outside the screen does not crash. test_play_sound_with_missing_file() verifies graceful error handling.
  • This is the “abstract test” or “interface compliance test” pattern. When someone creates a GodotEngine implementation, they run the same test suite against it. If any test fails, their implementation violates the contract.
  • This approach directly validates the Liskov Substitution Principle: any GameEngine subtype should be substitutable without breaking the contract. The test suite is the executable specification of that contract.
  • In Python, I would use pytest with parameterized fixtures: @pytest.fixture(params=[Unity2DEngine, PygameEngine, GodotEngine]) that yields an instance of each implementation.
Follow-up: What if different implementations have legitimately different behaviors (e.g., PygameEngine does not support 3D rendering)?Then the abstract interface is too broad — it is an ISP violation. Split GameEngine into GameEngine2D and GameEngine3D. Implementations choose which interface(s) to implement. The contract test suite for GameEngine2D only tests 2D operations. This keeps the tests valid and prevents false failures on implementations that legitimately do not support certain features.

🏃 Next: SOLID Principles

Now that you understand OOP, let’s learn the SOLID principles - five rules that make your OOP code even better!

Continue to SOLID Principles →

Learn the five principles that separate good OOP from great OOP!