---
title: "0.4.1 OOP: classes, inheritance, composition, dataclasses"
description: "Orders, positions and portfolios as Python classes: immutable dataclasses, priority ordering, inheritance for order types and composition."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/python-classes-inheritance-dataclasses/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/python-classes-inheritance-dataclasses.png"
---

# 0.4.1 · Object-oriented programming: classes, inheritance, composition and dataclasses

> A trading system handles objects with state and rules: orders that must not change, positions that do change and portfolios that group positions. This lesson models them with classes and dataclasses, and explains when to inherit and when to compose.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.4 · Intermediate Python
- Duration: 45 min
- Prerequisites: [0.3.5 · Functions: arguments, *args and **kwargs, lambdas and scope](https://pythonparatrading.com/en/python-trading-course/level-0/python-functions-arguments-scope/), [0.3.6 · Exceptions; modules and packages, with Python 3.15 lazy imports](https://pythonparatrading.com/en/python-trading-course/level-0/python-exceptions-modules-packages/)

## By the end of this lesson

- Write a class with `__init__`, methods, `__repr__` and `__eq__`, and recognize the boilerplate a dataclass saves.
- Model an immutable order with `@dataclass(frozen=True)`, validate it in `__post_init__` and sort orders by price-time priority.
- Build a portfolio by composition that computes its value and its realized and unrealized P&L.
- Decide between inheritance and composition with the “is a” versus “has a” rule.

## Why classes

So far an order's data fit in a dictionary: `{"simbolo": "SINT", "lado": "compra", "cantidad": 100}` (symbol, side
buy, quantity). It works, but it protects against nothing. A typo (`"cantdad"`) silently creates a new key, nothing
prevents a negative quantity and the notional calculation lives in a loose function you have to remember to call.

A **class** brings together in one place the data (attributes) and what can be done with them (methods). Each object
created from the class is an **instance**. In a trading system there are three obvious candidates:

| Object | What it holds | Does it change? | What it does |
|---|---|---|---|
| Order | symbol, side, quantity, price | No: once sent, it is canceled or replaced | Computes its notional, validates itself |
| Position | symbol, quantity, average price, realized P&L | Yes: with each fill | Applies fills, computes its value |
| Portfolio | cash and positions | Yes | Executes orders, computes value and P&L |

The “Does it change?” column decides a good part of the design. Orders will be immutable; positions and the
portfolio, not.

## A class written by hand

The basic form of a class: `__init__` receives the data and stores it in `self`, the instance itself. Methods are
functions that receive `self` as their first argument. Those that start and end with a double underscore are
**special methods**: Python calls them on its own. `__repr__` decides how the object is displayed and `__eq__`, when
two objects are equal.

```python
class OrdenManual:
    def __init__(self, simbolo, lado, cantidad, precio):
        self.simbolo = simbolo
        self.lado = lado
        self.cantidad = cantidad
        self.precio = precio

    def importe(self):
        return self.cantidad * self.precio

    def __repr__(self):
        return (f"OrdenManual({self.simbolo!r}, {self.lado!r}, "
                f"{self.cantidad}, {self.precio})")

    def __eq__(self, otra):
        if not isinstance(otra, OrdenManual):
            return NotImplemented
        return ((self.simbolo, self.lado, self.cantidad, self.precio)
                == (otra.simbolo, otra.lado, otra.cantidad,
                    otra.precio))

a = OrdenManual("SINT", "compra", 100, 142.22)
b = OrdenManual("SINT", "compra", 100, 142.22)
print(a)
print(f"Notional: {a.importe():.2f}")
print(a == b, a is b)
```
```output
OrdenManual('SINT', 'compra', 100, 142.22)
Notional: 14222.00
True False
```

Without `__repr__`, `print(a)` would show something like `<OrdenManual object at 0x…>`, useless in a trade log.
Without `__eq__`, `a == b` would compare identity (whether they are the same object in memory) and would give `False`
for two identical orders. With both methods, `a == b` is `True` even though `a is b` is still `False`: they are two
objects with the same content.

The notional (`importe`) is printed with two decimals as a precaution. `100 * 142.22` gives exactly `14222.0`, but
`3 * 142.22` gives `426.65999999999997`: it is the floating-point problem from lesson 0.3.1, and the reason price
accounting uses `decimal`.

There is a lot of repeated code: each field appears four times in `__init__`, once more in `__repr__` and twice in
`__eq__`. If tomorrow a field is added and forgotten in `__eq__`, two different orders will be “equal”.

## Dataclasses: the same result without repetition

The `@dataclass` decorator (the `dataclasses` module, in the standard library) reads the class annotations and writes
`__init__`, `__repr__` and `__eq__` for you. Annotations (`simbolo: str`) are covered in depth in lesson 0.4.2; here
it is enough to know that each `name: type` line declares a field.

Two options fit an order:

- `frozen=True`: the instance cannot be modified after it is created. A sent order does not change; if you want a
  different quantity, you create another order.
- `slots=True`: attributes are stored in fixed slots instead of in a dictionary. It uses less memory and makes it
  impossible to create attributes by mistake.

Validation goes in `__post_init__`, which the dataclass calls at the end of its `__init__`. The notional is a
**property** (`@property`): it is computed when read, without parentheses, and is not stored.

```python
from dataclasses import dataclass, field, replace

LADOS = ("compra", "venta")

@dataclass(frozen=True, slots=True)
class Orden:
    simbolo: str
    lado: str
    cantidad: int
    precio: float

    def __post_init__(self):
        if self.lado not in LADOS:
            raise ValueError(f"invalid side: {self.lado!r}")
        if self.cantidad <= 0:
            raise ValueError(f"non-positive quantity: {self.cantidad}")

    @property
    def importe(self) -> float:
        return self.cantidad * self.precio

orden = Orden("SINT", "compra", 100, 142.22)
print(orden)
print(orden == Orden("SINT", "compra", 100, 142.22))
print(f"{orden.importe:.2f}")
```
```output
Orden(simbolo='SINT', lado='compra', cantidad=100, precio=142.22)
True
14222.00
```

The guarantees are checked as soon as someone tries to get around them. Modifying a field of a frozen order raises
`FrozenInstanceError`:

```python raises
orden.cantidad = 200
```
```output
dataclasses.FrozenInstanceError: cannot assign to field 'cantidad'
```

And a badly built order never comes into existence (`"comprar"`, “to buy”, is not one of the valid sides `"compra"`
and `"venta"`):

```python raises
Orden("SINT", "comprar", 100, 142.22)
```
```output
ValueError: invalid side: 'comprar'
```

To change something you create a new order with `replace`, which copies all fields except the ones given and goes
through `__post_init__` again. Since frozen orders have `__hash__`, they can also be put in a set, which is useful,
for example, to discard duplicate orders that a signal generated twice:

```python
reducida = replace(orden, cantidad=50)
print(reducida)
print(orden.cantidad)

pendientes = {orden, Orden("SINT", "compra", 100, 142.22), reducida}
print(len(pendientes))
```
```output
Orden(simbolo='SINT', lado='compra', cantidad=50, precio=142.22)
100
2
```

The original still has 100 shares and the set holds two orders, not three: the first two are equal.

## Order and priority: price-time

An order book executes first the order with the best price and, at equal price, the one that arrived first. That is
**price-time** priority. With `order=True`, a dataclass compares its fields as a tuple, in the order in which they
are declared, so it is enough to declare the price first and then the arrival (`llegada`). Fields that must not count
in the comparison carry `field(compare=False)`:

```python
@dataclass(frozen=True, order=True)
class OrdenLibro:
    precio: float
    llegada: int
    id: str = field(compare=False)
    cantidad: int = field(compare=False)

ventas = [
    OrdenLibro(142.30, 3, "v3", 200),
    OrdenLibro(142.25, 5, "v5", 100),
    OrdenLibro(142.30, 1, "v1", 50),
    OrdenLibro(142.25, 2, "v2", 300),
]
for o in sorted(ventas):
    print(o.id, o.precio, o.llegada, o.cantidad)
```
```output
v2 142.25 2 300
v5 142.25 5 100
v1 142.3 1 50
v3 142.3 3 200
```

For sells, the best price is the lowest, which is exactly the natural order. For buys, the best price is the highest
and the trick does not work as is: exercise 2 solves it with a sort key.

## Position and portfolio: composition

A position does change: each fill modifies its quantity and its average price, and reducing it **realizes** a profit
or a loss. It is a regular dataclass, without `frozen`. The `aplicar` (apply) method receives a signed quantity
(positive to buy, negative to sell):

```python
@dataclass
class Posicion:
    simbolo: str
    cantidad: int = 0
    precio_medio: float = 0.0
    pnl_realizado: float = 0.0

    def aplicar(self, cantidad: int, precio: float) -> None:
        """Adds a fill: cantidad > 0 buys; < 0 sells."""
        if self.cantidad == 0 or self.cantidad * cantidad > 0:
            total = self.cantidad + cantidad  # opens or increases
            self.precio_medio = (self.cantidad * self.precio_medio
                                 + cantidad * precio) / total
            self.cantidad = total
            return
        signo = 1 if self.cantidad > 0 else -1  # reduces
        cerrada = min(abs(cantidad), abs(self.cantidad))
        self.pnl_realizado += signo * cerrada * (
            precio - self.precio_medio)
        self.cantidad += cantidad
        if self.cantidad == 0:
            self.precio_medio = 0.0
        elif self.cantidad * signo < 0:  # it has flipped
            self.precio_medio = precio

    def valor(self, precio: float) -> float:
        return self.cantidad * precio

    def pnl_no_realizado(self, precio: float) -> float:
        return self.cantidad * (precio - self.precio_medio)

p = Posicion("RVAR")
p.aplicar(100, 150.0)
p.aplicar(50, 160.0)
p.aplicar(-80, 166.0)
print(p)
```
```output
Posicion(simbolo='RVAR', cantidad=70, precio_medio=153.33333333333334, pnl_realizado=1013.3333333333326)
```

The two buys leave 150 shares at an average price of 153.33; selling 80 at 166 realizes
80 × (166 − 153.33) = 1,013.33 of profit, and 70 shares remain at the same average price. The dataclass `repr` shows
the floats as they are (`153.33333333333334`, `1013.3333333333326`): to read them, they are rounded when printed, as
in the next cell.

The portfolio *is* not a position: it *has* positions, and cash. That is **composition**: an object that contains
others and delegates work to them. The portfolio only knows how to turn an `Orden` into a signed fill, charge the
commission and add up.

A detail of dataclasses: a mutable default value, such as an empty dictionary, would be **the same object** for all
portfolios. The dataclass does not allow it:

```python raises
@dataclass
class CarteraMal:
    efectivo: float
    posiciones: dict = {}
```
```output
ValueError: mutable default <class 'dict'> for field posiciones is not allowed: use default_factory
```

The correct form is `field(default_factory=dict)`, which creates a new dictionary for each instance:

```python
@dataclass
class Cartera:
    efectivo: float
    comision: float = 1.0  # euros per order
    posiciones: dict[str, Posicion] = field(default_factory=dict)

    def ejecutar(self, orden: Orden) -> None:
        signo = 1 if orden.lado == "compra" else -1
        if orden.simbolo not in self.posiciones:
            self.posiciones[orden.simbolo] = Posicion(orden.simbolo)
        self.posiciones[orden.simbolo].aplicar(
            signo * orden.cantidad, orden.precio)
        self.efectivo -= signo * orden.importe + self.comision

    def valor(self, precios: dict[str, float]) -> float:
        return self.efectivo + sum(
            p.valor(precios[s]) for s, p in self.posiciones.items())

    def pnl(self, precios: dict[str, float]) -> tuple[float, float]:
        realizado = sum(p.pnl_realizado
                        for p in self.posiciones.values())
        no_realizado = sum(p.pnl_no_realizado(precios[s])
                           for s, p in self.posiciones.items())
        return realizado, no_realizado

cartera = Cartera(efectivo=50_000)
for o in [Orden("RVAR", "compra", 100, 150.0),
          Orden("ORO", "compra", 50, 165.0),
          Orden("RVAR", "compra", 50, 160.0),
          Orden("RVAR", "venta", 80, 166.0)]:
    cartera.ejecutar(o)

# closes on 2025-12-31 in cartera_sint.csv
cierres = {"RVAR": 164.35, "RFIJ": 102.29, "ORO": 171.44,
           "TECH": 77.82}
realizado, no_realizado = cartera.pnl(cierres)
print(f"Cash:         {cartera.efectivo:>10,.2f}")
print(f"Total value:  {cartera.valor(cierres):>10,.2f}")
print(f"Realized:     {realizado:>10,.2f}")
print(f"Unrealized:   {no_realizado:>10,.2f}")
print(f"Commissions:  {4 * cartera.comision:>10,.2f}")
```
```output
Cash:          32,026.00
Total value:   52,102.50
Realized:       1,013.33
Unrealized:     1,093.17
Commissions:        4.00
```

The figures add up: the total value minus the initial 50,000 equals the realized P&L plus the unrealized P&L minus the
four commissions. That identity is a good candidate for an automated test (lesson 0.4.5): if one day it stops
holding, there is a bug in the accounting.

## Inheritance: variants of an order

**Inheritance** creates a class from another: the subclass receives the attributes and methods of the base class and
can add or override its own. It fits when an object *is a* particular case of another. A market order, a limit order
and a stop order are orders; what changes between them is **when they execute**.

The base class declares the `se_ejecuta` (is executed) method as abstract (`abc.ABC` and `@abstractmethod`): you
cannot create a generic order, only one of its variants. Each subclass adds its fields and its rule:

```python
from abc import ABC, abstractmethod

@dataclass(frozen=True)
class OrdenBase(ABC):
    simbolo: str
    lado: str
    cantidad: int

    @abstractmethod
    def se_ejecuta(self, precio: float) -> bool: ...

@dataclass(frozen=True)
class OrdenMercado(OrdenBase):
    def se_ejecuta(self, precio: float) -> bool:
        return True

@dataclass(frozen=True)
class OrdenLimitada(OrdenBase):
    limite: float

    def se_ejecuta(self, precio: float) -> bool:
        if self.lado == "compra":
            return precio <= self.limite
        return precio >= self.limite

@dataclass(frozen=True)
class OrdenStop(OrdenBase):
    stop: float

    def se_ejecuta(self, precio: float) -> bool:
        if self.lado == "venta":
            return precio <= self.stop
        return precio >= self.stop

ordenes = [OrdenMercado("SINT", "compra", 10),
           OrdenLimitada("SINT", "compra", 10, limite=140.0),
           OrdenStop("SINT", "venta", 10, stop=138.0)]
for precio in (142.0, 139.5, 137.8):
    activas = [type(o).__name__ for o in ordenes
               if o.se_ejecuta(precio)]
    print(precio, activas)
```
```output
142.0 ['OrdenMercado']
139.5 ['OrdenMercado', 'OrdenLimitada']
137.8 ['OrdenMercado', 'OrdenLimitada', 'OrdenStop']
```

The loop does not ask what type each order is: it calls `se_ejecuta` and each object answers with its rule. That is
**polymorphism**, and it is what makes inheritance useful. Adding a stop-limit order does not require touching the
loop, only writing one more subclass. Trying to create the base class fails, as intended:

```python raises
OrdenBase("SINT", "compra", 10)
```
```output
TypeError: Can't instantiate abstract class OrdenBase without an implementation for abstract method 'se_ejecuta'
```

A dataclass trap with inheritance: the subclass fields go after the base fields. If the base had a field with a
default value, the subclass could not add one without it (`TypeError: non-default argument follows default
argument`). The way out is to declare the fields as keyword-only with `@dataclass(kw_only=True)`, or not to put
default values in the base.

## Inheritance or composition

The two relationships are drawn differently. The “is a” arrows are inheritance; the “has” and “executes” arrows,
composition:

```mermaid
flowchart LR
  M[OrdenMercado] -->|is a| B[OrdenBase]
  L[OrdenLimitada] -->|is a| B
  S[OrdenStop] -->|is a| B
  C[Cartera] -->|has| P[Posicion]
  C -->|executes| O[Orden]
```

The practical rule: **inherit only if the sentence “X is a Y” is true and will remain so**; in any other case,
compose. Typical mistakes in trading code almost always go in the same direction, inheriting too much:

- `class Estrategia(Cartera)`: a strategy is not a portfolio. If tomorrow two strategies share a portfolio, or one
  strategy splits capital between two, inheritance does not allow it.
- `class MiEstrategia(EstrategiaMediasMoviles, GestorRiesgo, ConectorBroker)`: multiple inheritance to “gather
  functions”. Each base class can override methods of the others, and testing the strategy requires a broker.
- Five-level hierarchies (`Estrategia` → `EstrategiaTendencia` → `EstrategiaMedias` → …) in which a change at the
  top breaks something at the bottom.

With composition, the strategy receives its pieces when it is created. Changing the risk manager, or using a fake one
in a test, does not require touching the strategy:

```python
@dataclass
class LimiteRiesgo:
    max_importe: float

    def permite(self, orden: Orden) -> bool:
        return orden.importe <= self.max_importe

@dataclass
class Ejecutor:
    cartera: Cartera
    riesgo: LimiteRiesgo
    rechazadas: list[Orden] = field(default_factory=list)

    def enviar(self, orden: Orden) -> bool:
        if not self.riesgo.permite(orden):
            self.rechazadas.append(orden)
            return False
        self.cartera.ejecutar(orden)
        return True

ejecutor = Ejecutor(Cartera(efectivo=20_000),
                    LimiteRiesgo(max_importe=10_000))
print(ejecutor.enviar(Orden("TECH", "compra", 100, 77.82)))
print(ejecutor.enviar(Orden("ORO", "compra", 100, 171.44)))
print(ejecutor.cartera.posiciones)
print([o.simbolo for o in ejecutor.rechazadas])
```
```output
True
False
{'TECH': Posicion(simbolo='TECH', cantidad=100, precio_medio=77.82, pnl_realizado=0.0)}
['ORO']
```

The TECH buy (€7,782) passes the limit; the ORO buy (€17,144) does not, and is recorded as rejected. For a different
limit, for example on the portfolio's total exposure, another class with a `permite` (allows) method with the same
signature is enough. How to formally declare that “common signature” (a `Protocol`) is covered in lesson 0.4.2.

<aside class="callout" data-kind="nota">

Classes are not mandatory. A function that receives prices and returns a series (like `senal_cruce` in lesson 0.1.1)
does not need a class: it keeps no state between calls. Use a class when there is state to protect (a position) or
variants with the same interface (the order types).

</aside>

## Exercises

1. Add to `Orden` an optional field `etiqueta: str = ""` (a label, for example the name of the strategy that generated
   it) that does not count when comparing orders. Check that two orders that are equal except for the label are
   equal.

**Solution.**

```python
@dataclass(frozen=True, slots=True)
class OrdenEtiquetada:
    simbolo: str
    lado: str
    cantidad: int
    precio: float
    etiqueta: str = field(default="", compare=False)

x = OrdenEtiquetada("SINT", "compra", 100, 142.22, "crossover")
y = OrdenEtiquetada("SINT", "compra", 100, 142.22, "breakout")
print(x == y, hash(x) == hash(y))
```
```output
True True
```

With `compare=False` the field is left out of `__eq__` and `__hash__`: the two orders are equal and land in the same
slot of a set. The field with a default value goes at the end, after those without one.

2. Sort these buy orders by price-time priority (best price = the highest; at equal price, the one that arrived
   first): `OrdenLibro(142.20, 4, "c4", 100)`, `OrdenLibro(142.25, 6, "c6", 50)`,
   `OrdenLibro(142.20, 1, "c1", 200)`.

**Solution.**

```python
compras = [OrdenLibro(142.20, 4, "c4", 100),
           OrdenLibro(142.25, 6, "c6", 50),
           OrdenLibro(142.20, 1, "c1", 200)]
for o in sorted(compras, key=lambda o: (-o.precio, o.llegada)):
    print(o.id, o.precio, o.llegada)
```
```output
c6 142.25 6
c1 142.2 1
c4 142.2 4
```

The key `(-precio, llegada)` inverts the price and keeps the time: first 142.25 and, at 142.20, the order that
arrived at instant 1 before the one from instant 4.

3. With the lesson's `Cartera`, open a short: sell 40 TECH at 80 and buy back 40 at 77.82. What is the realized
   P&L? What quantity and average price remain in the position?

**Solution.**

```python
corta = Cartera(efectivo=10_000)
corta.ejecutar(Orden("TECH", "venta", 40, 80.0))
corta.ejecutar(Orden("TECH", "compra", 40, 77.82))
print(corta.posiciones["TECH"])
print(f"Cash: {corta.efectivo:,.2f}")
```
```output
Posicion(simbolo='TECH', cantidad=0, precio_medio=0.0, pnl_realizado=87.20000000000027)
Cash: 10,085.20
```

The short gains 40 × (80 − 77.82) = 87.20 and the position goes back to zero with a zero average price. The `repr`
shows `87.20000000000027`, not `87.2`: `80 - 77.82` gives `2.180000000000007` in floating point (lesson 0.3.1). It is
a representation error, not an accounting error, but it requires two things: rounding when displaying and comparing
with a tolerance in tests (`pytest.approx`, lesson 0.4.5), never with `==`. Cash goes up by those 87.20 minus the two
€1 commissions. `Posicion.aplicar` treats longs and shorts the same because it works with signed quantities.

## Self-assessment

1. An order already sent to the broker must not change its quantity by accident. Which option guarantees that in Python?

   - A. A dictionary with the `cantidad` key
   - B. A dataclass with `frozen=True`: assigning a field raises `FrozenInstanceError`
   - C. A regular class with a comment saying “do not modify”
   - D. A dataclass with `order=True`

   *Answer B: A dataclass with `frozen=True`: assigning a field raises `FrozenInstanceError`.* `frozen=True` makes assigning a field after creating the object raise an exception. To “change” an order you create another one with `dataclasses.replace`, and the original stays intact. `order=True` only adds comparisons.

2. A dataclass has the field `posiciones: dict = {}`. What happens when the class is defined?

   - A. Nothing: each portfolio will have its own empty dictionary
   - B. All portfolios share the same dictionary, without warning
   - C. It raises `ValueError`: mutable defaults are declared with `field(default_factory=dict)`
   - D. It raises `TypeError` when the first portfolio is created

   *Answer C: It raises `ValueError`: mutable defaults are declared with `field(default_factory=dict)`.* A default `{}` would be the same object for every instance. The dataclass detects it when the class is defined and raises `ValueError`; `default_factory` creates a new dictionary per instance.

3. With `@dataclass(order=True)` and the fields `precio` and `llegada` (in that order), how does `sorted()` sort a list of sell orders?

   - A. By price and, at equal price, by arrival: the price-time priority of an order book
   - B. By price only
   - C. By the order in which the objects were created
   - D. Randomly, because the objects cannot be compared

   *Answer A: By price and, at equal price, by arrival: the price-time priority of an order book.* `order=True` compares the fields as a tuple, in the order in which they are declared. Fields with `field(compare=False)` are left out of the comparison.

4. A strategy needs a portfolio and a risk manager. Which design follows the lesson's rule?

   - A. `class Estrategia(Cartera, GestorRiesgo)`
   - B. `class Estrategia(Cartera)` and risk as methods of the strategy
   - C. A strategy that has a portfolio and a risk manager as attributes (composition)
   - D. Global variables shared by all modules

   *Answer C: A strategy that has a portfolio and a risk manager as attributes (composition).* A strategy is not a portfolio: it has one. With composition you can change the risk manager without touching the strategy, and each piece is tested separately.

## Key points

- A class brings data and behavior together. `__repr__` and `__eq__` decide how it prints and when two objects are equal.
- `@dataclass` writes `__init__`, `__repr__` and `__eq__` for you; `frozen=True` prevents modifying an object and makes it usable in sets and as a dictionary key.
- `order=True` compares the fields as a tuple: an order book's price-time priority comes from declaring the fields in that order.
- Mutable defaults go with `field(default_factory=…)`; the dataclass prevents the classic mistake of sharing a list or a dictionary between instances.
- Inherit when the object is a variant of another (a limit order is an order); compose when it uses it (a strategy has a portfolio).

## Resources

### Research

- [Finite-State Machines for Quantitative Trading Execution](https://pythonparatrading.com/en/finite-state-machines-trading-python/) · Research-0029 · 47 min

### Documentation

- [Python 3.14 documentation: dataclasses](https://docs.python.org/3.14/library/dataclasses.html) · docs.python.org
- [Python tutorial in Spanish: classes](https://docs.python.org/3/tutorial/classes.html) · docs.python.org

### Books

- [Luciano Ramalho, Fluent Python, 2nd ed. (O'Reilly, 2022)](https://www.fluentpython.com/) · fluentpython.com

### Data

- [Level 0 data: closes of four fictitious assets (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/cartera_sint.csv) · Download · cartera_sint.csv

Previous lesson: [0.3.6 · Exceptions; modules and packages, with Python 3.15 lazy imports](https://pythonparatrading.com/en/python-trading-course/level-0/python-exceptions-modules-packages/)

Next lesson: [0.4.2 · Typing: type hints, mypy or ty, and Pydantic for configurations](https://pythonparatrading.com/en/python-trading-course/level-0/python-type-hints-and-pydantic/)
