> ## Documentation Index
> Fetch the complete documentation index at: https://fastapi2day.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Python OOP & Classes: Blueprints, Inheritance & More

> Define classes and instances, apply inheritance and encapsulation, use properties, abstract base classes, and introspection tools.

Object-Oriented Programming (OOP) is a paradigm that groups related data (**attributes**) and behaviour (**methods**) into reusable blueprints called **classes**. An individual object built from a class is an **instance**. Python's OOP model is highly flexible — classes are first-class objects, type checking is dynamic, and you have precise control over encapsulation and inheritance. This page walks you through everything from basic class definitions to advanced features like abstract base classes and class variables.

## Classes and Instances

A **class** is a blueprint. An **instance** is a concrete object built from that blueprint.

```python theme={null}
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def study(self):
        print(f"{self.name} is studying.")

    def introduce(self):
        print(f"My name is {self.name}.")
        print(f"I am {self.age} years old.")

# Create an object
student1 = Student("Alice", 20)

print(student1.name)    # Alice
student1.introduce()
student1.study()
```

Key concepts demonstrated above:

* **Class**: `Student`
* **Object (instance)**: `student1`
* **Attributes**: `name`, `age`
* **Methods**: `introduce()`, `study()`
* **Constructor**: `__init__()` initialises instance attributes

## Attributes and Methods

### Instance Methods

Functions inside a class that operate on a specific instance. They always accept `self` as the first parameter:

```python theme={null}
class Account:
    def __init__(self, owner: str, balance: float):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float):
        self.balance += amount
        return self.balance
```

### Class Attributes and `@classmethod`

Class attributes are shared by **all** instances. Class methods accept `cls` instead of `self` and are used for factory methods or modifying class-level state:

```python theme={null}
class Account:
    bank_name = "State Bank of India"   # class attribute

    def __init__(self, owner: str, balance: float):
        self.owner = owner
        self.balance = balance

    @classmethod
    def update_bank(cls, new_name: str):
        cls.bank_name = new_name
```

### Static Methods (`@staticmethod`)

Static methods do not receive `self` or `cls`. They are plain utility functions grouped inside the class namespace:

```python theme={null}
class Account:
    @staticmethod
    def is_valid_amount(amount: float) -> bool:
        return amount > 0
```

### Type Checking: `type()` vs `isinstance()`

```python theme={null}
class Animal: pass
class Dog(Animal): pass

buddy = Dog()

print(type(buddy) is Dog)        # True  — exact class check
print(type(buddy) is Animal)     # False — does not check hierarchy
print(isinstance(buddy, Animal)) # True  — checks inheritance
```

<Tip>
  Prefer `isinstance()` in most code. It respects the inheritance hierarchy and is the Pythonic way to check types.
</Tip>

## Inheritance

Inheritance lets a **child class** inherit attributes and methods from a **parent class**, extending or overriding behaviour as needed:

```python theme={null}
class Animal:
    def __init__(self, name: str):
        self.name = name

    def eat(self):
        return f"{self.name} is eating"

class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name)   # initialise parent attributes
        self.breed = breed

    def bark(self):
        return "Woof!"

buddy = Dog("Buddy", "Golden Retriever")
print(buddy.eat())    # Buddy is eating  (inherited)
print(buddy.bark())   # Woof!
```

### Passing Arguments via `__init_subclass__`

You can pass configuration to a parent class at **class-definition time** using `__init_subclass__`:

```python theme={null}
class DatabaseModel:
    table_name: str

    def __init_subclass__(cls, table: str, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.table_name = table

class UserProfile(DatabaseModel, table="users"):
    pass

print(UserProfile.table_name)   # users
```

This runs at import time — before any instances are created — and is the pattern used by ORMs like SQLAlchemy.

## Encapsulation

Encapsulation restricts direct access to internal state to prevent accidental modification:

* **Protected** (`_name`): Convention only — Python does not enforce it.
* **Private** (`__pin`): Triggers **name mangling** (`_ClassName__pin`), making direct external access raise an `AttributeError`.

```python theme={null}
class User:
    def __init__(self, username: str, pin: str):
        self.username = username    # public
        self._status = "active"     # protected (by convention)
        self.__pin = pin            # private (name-mangled)
```

## Properties

Properties let you attach validation logic to attribute access using `@property` getters and setters:

```python theme={null}
class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self._price = price

    @property
    def price(self) -> float:
        return self._price

    @price.setter
    def price(self, value: float):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

item = Product("Laptop", 999.0)
print(item.price)    # 999.0  — getter
item.price = 1050.0  # setter (validates the value)
```

## Abstract Classes

Abstract Base Classes (ABCs) define an **interface contract** — subclasses must implement every abstract method:

```python theme={null}
from abc import ABC, abstractmethod

class BaseRepository(ABC):
    @abstractmethod
    def save(self, data: dict) -> None:
        pass

# repo = BaseRepository()   # TypeError: Can't instantiate abstract class

class SQLRepository(BaseRepository):
    def save(self, data: dict) -> None:
        print(f"Saving {data} to Database")
```

## Introspection

Python provides built-in tools to inspect objects at runtime:

```python theme={null}
class User:
    username: str
    email: str

    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email

# Inspect class-level type annotations
print(User.__annotations__)
# {'username': <class 'str'>, 'email': <class 'str'>}

# Inspect instance attributes
u = User("alice", "alice@example.com")
print(u.__dict__)
# {'username': 'alice', 'email': 'alice@example.com'}
```

## Type Annotations, Class Variables, and Instance Variables

Understanding what each declaration means is critical when working with Python, Pydantic, and dataclasses.

### Type Annotation Only

A bare annotation describes the **expected type** but does not create an attribute:

```python theme={null}
class Product:
    name: str    # Only a type hint — no attribute exists yet
    price: float
```

Accessing `Product.name` raises `AttributeError`.

### Instance Variables

Created when a value is assigned to `self` inside `__init__`:

```python theme={null}
class Product:
    def __init__(self, name, price):
        self.name = name    # instance variable
        self.price = price
```

Each object gets its own independent copy.

### Class Variables

Shared by every instance. Annotate with `ClassVar` to make intent explicit:

```python theme={null}
from typing import ClassVar

class Product:
    category: ClassVar[str] = "Electronics"
```

### Behaviour Across Frameworks

| Declaration                    | Plain Python         | Pydantic `BaseModel`                |
| ------------------------------ | -------------------- | ----------------------------------- |
| `name: str`                    | Type annotation only | Required model field                |
| `name: str = "Siva"`           | Class variable       | Model field with a default          |
| `name: ClassVar[str] = "Siva"` | Class variable       | Class variable (ignored as field)   |
| `self.name = value`            | Instance variable    | N/A (Pydantic generates `__init__`) |

<Tip>
  Always use `ClassVar` when declaring class-level constants. It prevents Pydantic and dataclasses from accidentally treating the attribute as a model field.
</Tip>
