---
title: "0.3.3 Python lists, tuples, dictionaries and sets"
description: "Python's four collections with trading data: watchlists, OHLC bars, portfolios, index universes and returns with comprehensions."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/python-lists-tuples-dicts-sets/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/python-lists-tuples-dicts-sets.png"
---

# 0.3.3 · Lists, tuples, dictionaries, sets and comprehensions

> A series of closes, an OHLC bar, a portfolio and the universe of an index are collections of data, and each one has its own structure in Python. The lesson teaches all four and comprehensions, the short way to build them.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.3 · Language fundamentals
- Duration: 50 min
- Prerequisites: [0.3.2 · Strings, f-strings and formatting numbers and dates](https://pythonparatrading.com/en/python-trading-course/level-0/python-strings-f-strings-formatting/)

## By the end of this lesson

- Create and modify lists (a watchlist, a series of closes) with indexes, slices and methods such as `append`, `remove` and `sort`.
- Represent an OHLC bar with a tuple and unpack it into variables.
- Value a portfolio with dictionaries of positions and prices, and read keys that may be missing with `get`.
- Compute additions to and deletions from an index with set operations, and daily returns with a comprehension.

## Collections: many pieces of data under one name

So far each variable held one piece of data: a price, a quantity, a ticker (lessons 0.3.1 and 0.3.2). A trading
program almost always works with many: the closes of a month, the assets being watched, the positions of a portfolio,
the components of an index. Python has four built-in **collections** to hold them, and each one meets a different
need:

| Collection | Literal | Ordered? | Modifiable? | Duplicates? | Typical use in trading |
|---|---|---|---|---|---|
| List (`list`) | `[139.32, 139.23]` | Yes | Yes | Yes | Series of closes, watchlist, pending orders |
| Tuple (`tuple`) | `(140.01, 143.40, 139.67, 142.22)` | Yes | No | Yes | An OHLC bar, a fixed record |
| Dictionary (`dict`) | `{"SINT": 100, "ORO": 20}` | By insertion | Yes | Unique keys | Portfolio, prices by ticker, parameters |
| Set (`set`) | `{"SINT", "ORO"}` | No | Yes | No | Universe of an index, unique symbols |

## Lists: series and watchlists

A **list** is an ordered sequence of values between square brackets. It is indexed and sliced exactly like a string
(lesson 0.3.2): the first element is `[0]`, the last `[-1]`, and `[inicio:fin]` returns a new list with that stretch.
SINT's closes for the second half of December 2025 (from the file `sint_diario.csv`, link in “Resources”) are a list of
floats:

```python
cierres = [146.36, 145.13, 144.22, 143.66, 139.91, 139.43,
           139.36, 139.32, 139.23, 139.56, 139.98, 142.22]
print(len(cierres), cierres[0], cierres[-1])
print(cierres[-3:])
print(max(cierres), min(cierres))
print(sum(cierres) / len(cierres))
```
```output
12 146.36 142.22
[139.56, 139.98, 142.22]
146.36 139.23
141.53166666666667
```

`len`, `max`, `min` and `sum` work with any collection of numbers. `cierres[-3:]` are the last three closes, and the
last line is the mean of the series. Along the same lines, `cierres[-20:]` would give the last twenty sessions of a
longer series: that is how the computation of a moving average starts.

Unlike strings, lists are **modifiable**. Their methods change the list itself and return nothing (they return
`None`). A watchlist, the list of assets being watched, is the natural example:

```python
watchlist = ["SINT", "RVAR", "ORO"]
watchlist.append("TECH")      # appends at the end
watchlist.insert(0, "RFIJ")   # inserts at a position
watchlist.remove("ORO")       # removes by value
ultimo = watchlist.pop()      # removes and returns the last one
print(watchlist, ultimo)

watchlist.sort()
print(watchlist, "SINT" in watchlist, watchlist.index("SINT"))
print(sorted(cierres)[:3])
```
```output
['RFIJ', 'SINT', 'RVAR'] TECH
['RFIJ', 'RVAR', 'SINT'] True 2
[139.23, 139.32, 139.36]
```

`sort()` sorts the list in place; `sorted()` returns a new sorted list and leaves the original as it was. The
difference matters with a price series: sorting the closes in place destroys the time order, and with it any return
calculation that comes after. For a time series, `sorted`, never `sort`.

The most common trap with lists is this one: assigning a list to another name does not copy it. Both names point to
the **same** list, and whatever is done through one is seen in the other.

```python
pendientes = ["SINT", "ORO"]
para_enviar = pendientes          # the same object
para_enviar.append("TECH")
print(pendientes)

copia = pendientes.copy()         # a new object
copia.clear()
print(pendientes, copia)
print(para_enviar is pendientes, copia is pendientes)
```
```output
['SINT', 'ORO', 'TECH']
['SINT', 'ORO', 'TECH'] []
True False
```

The `is` operator asks whether two names point to the same object. In an order system, this error has consequences: a
script that “empties the copy” of the pending order list after sending them may be emptying the original. The same
trap shows up again with the default arguments of functions (lesson 0.3.5).

## Tuples: an OHLC bar

A **tuple** is a sequence like a list, but **immutable**: once created, it cannot be changed. It is written between
parentheses, or simply with commas. It is the right structure for a record with a fixed number of fields that must not
change, such as a price bar: open, high, low and close (OHLC).

```python
barra = ("2025-12-31", 140.01, 143.40, 139.67, 142.22)
fecha, apertura, maximo, minimo, cierre = barra
print(fecha, cierre)
print(f"Range: {maximo - minimo:.2f}  Body: {cierre - apertura:+.2f}")
print(barra[1:])
```
```output
2025-12-31 142.22
Range: 3.73  Body: +2.21
(140.01, 143.4, 139.67, 142.22)
```

The second line is **unpacking**: a tuple (or any sequence) is distributed over as many variables as it has elements.
It is what `base, cotizada = "EUR", "USD"` did in lesson 0.3.2: on the right there was a tuple without parentheses. If
the number of variables does not match the number of elements, Python raises an error.

Immutability is a guarantee: a historical bar should not change, and a tuple does not let it (`barra[4] = 150.0`
raises an error). A bar that could be modified by accident would be falsified historical data in the middle of a
backtest.

## Dictionaries: portfolio and prices by ticker

A **dictionary** maps **keys** to **values**. It is written between braces with `clave: valor` pairs, and it is looked
up by key, not by position. A portfolio is a dictionary of ticker → shares; prices, another of ticker → last close
(those of SINT and of the assets in `cartera_sint.csv` on December 31, 2025):

```python
cartera = {"SINT": 100, "ORO": 20, "RFIJ": 150}
precios = {"SINT": 142.22, "ORO": 171.44, "RFIJ": 102.29,
           "TECH": 77.82}

print(cartera["SINT"], len(cartera), "TECH" in cartera)
cartera["TECH"] = 30          # open a position
cartera["ORO"] += 10          # increase a position
del cartera["RFIJ"]           # close a position
print(cartera)
print(list(cartera.keys()), list(cartera.values()))
```
```output
100 3 False
{'SINT': 100, 'ORO': 30, 'TECH': 30}
['SINT', 'ORO', 'TECH'] [100, 30, 30]
```

Assigning to a key that does not exist creates it; assigning to one that exists replaces its value. `in` asks about the
keys, not the values. Since Python 3.7, dictionaries preserve the order in which the keys were inserted.

To value the portfolio you have to go through it. `items()` gives the key-value pairs, and the `for` loop (lesson
0.3.4) processes them one by one:

```python
valor_total = 0.0
for ticker, acciones in cartera.items():
    valor = acciones * precios[ticker]
    valor_total += valor
    print(f"{ticker:<5} {acciones:>4} x {precios[ticker]:>7.2f}"
          f" = {valor:>9,.2f}")
print(f"{'Total':<22}{valor_total:>9,.2f}")
```
```output
SINT   100 x  142.22 = 14,222.00
ORO     30 x  171.44 =  5,143.20
TECH    30 x   77.82 =  2,334.60
Total                 21,699.80
```

Reading a key that does not exist with square brackets is an error (`KeyError`), and in trading it happens often: a
ticker with no price that day, a misspelled symbol. If the absence is normal and has a reasonable value, `get` gives it
without an error:

```python
print(cartera.get("SINT", 0), cartera.get("BTC", 0))
print(precios.get("BTC"))
```
```output
100 0
None
```

Without a second argument, `get` returns `None`. Be careful not to use it to cover up errors: a portfolio that holds no
BTC holds 0 BTC, but a missing price is **not** a price of 0. Valuing a position at a price of 0 makes it disappear from
the portfolio's value without any warning. When it is better to let the error happen and how to handle it is covered in
lesson 0.3.6.

## Sets: the universe of an index

A **set** is a collection of unique elements with no order, written between braces without pairs. It is used for two
things: removing duplicates and doing set operations, which are exactly the questions asked about the universe of an
index. The components of an index change over time: companies that have grown join, and those that have fallen,
merged or gone bankrupt leave.

The example uses a fictional index with six components at two dates:

```python
universo_2024 = {"ALFA", "BETA", "GAMMA", "DELTA", "SIGMA", "OMEGA"}
universo_2025 = {"ALFA", "BETA", "DELTA", "SIGMA", "KAPPA", "LAMBDA"}

altas = universo_2025 - universo_2024
bajas = universo_2024 - universo_2025
siguen = universo_2024 & universo_2025
alguna_vez = universo_2024 | universo_2025
print("Additions:", sorted(altas))
print("Deletions:", sorted(bajas))
print("Remain:", len(siguen), " Ever:", len(alguna_vez))
print("Changes:", sorted(universo_2024 ^ universo_2025))
```
```output
Additions: ['KAPPA', 'LAMBDA']
Deletions: ['GAMMA', 'OMEGA']
Remain: 4  Ever: 8
Changes: ['GAMMA', 'KAPPA', 'LAMBDA', 'OMEGA']
```

| Operation | Operator | Question |
|---|---|---|
| Difference | `a - b` | What is in `a` and not in `b`? (additions or deletions) |
| Intersection | `a & b` | What is in both? |
| Union | `a \| b` | What is in either? |
| Symmetric difference | `a ^ b` | What is in only one? (all the changes) |

Sets have no order, so `print` would show them in any order; `sorted` turns them into a sorted list so that the output
is always the same.

The deletions line is the one that matters most in trading. If a backtest of the last ten years uses only **today's**
components, it never buys GAMMA or OMEGA: the companies that left, often after falling, are not on the list. The
backtest only sees the survivors and comes out better than it really would have been. This is **survivorship bias**,
and the only remedy is to use the universe that existed at each date, `alguna_vez` included:

*Fig. — Survivorship bias: the same index with and without the companies that disappeared (schematic).* Two lines over ten years from 100: the full universe, including delisted companies, ends well below the version that keeps only the survivors, highlighted.

The other use of sets is removing duplicates. The symbols traded in a trade log repeat; `set` keeps each one once:

```python
simbolos_operados = ["ORO", "SINT", "SINT", "SINT", "ORO", "TECH"]
unicos = set(simbolos_operados)
print(sorted(unicos), len(unicos))
```
```output
['ORO', 'SINT', 'TECH'] 3
```

## Comprehensions: returns in one line

A **comprehension** builds a collection from another one in a single expression: `[expression for element in
collection]`. It reads almost like a sentence: “the expression, for each element of the collection”. It is the
idiomatic way to transform a series.

A day's return is $r_t = P_t / P_{t-1} - 1$: it needs each close and the previous one. `zip` pairs the elements of two
collections; if you pass it the series and the series shifted by one position (`cierres[1:]`), each pair is (previous
close, close of the day):

```python
rentabilidades = [c1 / c0 - 1
                  for c0, c1 in zip(cierres, cierres[1:])]
print(len(rentabilidades))
print(" ".join([f"{r:+.2%}" for r in rentabilidades]))

peor = min(rentabilidades)
print(f"Worst day: {peor:+.2%}, session {rentabilidades.index(peor) + 1}")
```
```output
11
-0.84% -0.63% -0.39% -2.61% -0.34% -0.05% -0.03% -0.06% +0.24% +0.30% +1.60%
Worst day: -2.61%, session 4
```

Twelve closes give eleven returns: `zip` stops when the shorter list runs out. The second comprehension turns each
return into a formatted text, and `" ".join(…)` joins those texts into one, separated by a space. The header figure
plots those eleven returns; the worst one, on December 22, in amber.

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

At level 0 returns are computed with lists to understand what they do. In practice NumPy and pandas are used (module
0.5), which do the same calculation over millions of data points in one line and much faster:
`cierre.pct_change()`.

</aside>

A comprehension can **filter** with an `if` at the end, and there are versions for dictionaries and sets:

```python
subidas = [r for r in rentabilidades if r > 0]
print(f"Up days: {len(subidas)} of {len(rentabilidades)}")

valores = {t: n * precios[t] for t, n in cartera.items()}
total = sum(valores.values())
pesos = {t: round(v / total, 3) for t, v in valores.items()}
print(pesos)

operaciones = [("SINT", 7.43), ("ORO", -3123.56),
               ("SINT", -31.50), ("TECH", 1426.71)]
con_perdidas = {s for s, pnl in operaciones if pnl < 0}
print(sorted(con_perdidas))
```
```output
Up days: 3 of 11
{'SINT': 0.655, 'ORO': 0.237, 'TECH': 0.108}
['ORO', 'SINT']
```

Braces with `clave: valor` give a dictionary; without the colon, a set. The portfolio weights come out in two steps:
first the value of each position and then its fraction of the total.

Comprehensions are convenient as long as they fit on one or two lines and can be read at a glance. If you need more
than one `if`, or the expression takes up half a line, a normal `for` loop (lesson 0.3.4) is clearer.

## Which collection to use

The decision comes from three questions, in this order:

```mermaid
flowchart TB
  Q1{Key and value?} -- yes --> D[dict]
  Q1 -- no --> Q2{Unique only?}
  Q2 -- yes --> S[set]
  Q2 -- no --> Q3{Will it change?}
  Q3 -- yes --> L[list]
  Q3 -- no --> T[tuple]
```

If each piece of data is looked up by a name (a ticker, a parameter), dictionary. If what matters is membership (it is
in the index, it has been traded), set. If it is a sequence that grows or changes (a series that receives a new close
every day, an order queue), list. If it is a fixed record (a bar, a closed trade), tuple.

## Exercises

1. With the lesson's `cierres` list, compute the total return for the period (last close divided by the first, minus
   1) and the number of sessions in which the close fell relative to the previous one.

**Solution.**

```python
total_periodo = cierres[-1] / cierres[0] - 1
bajadas = [r for r in rentabilidades if r < 0]
print(f"Period return: {total_periodo:+.2%}")
print(f"Down sessions: {len(bajadas)}")
```
```output
Period return: -2.83%
Down sessions: 8
```

The total return only needs the first and the last close. The down sessions come from filtering the negative returns
with a comprehension.

2. The fictional index changes again in 2026: BETA and KAPPA leave and OMEGA and RHO join. Build `universo_2026` from
   `universo_2025` with set operations, and compute how many companies have been part of the index at some point in
   the three years.

**Solution.**

```python
universo_2026 = (universo_2025 - {"BETA", "KAPPA"}) | {"OMEGA", "RHO"}
print(sorted(universo_2026))
historico = universo_2024 | universo_2025 | universo_2026
print(len(historico), sorted(historico))
```
```output
['ALFA', 'DELTA', 'LAMBDA', 'OMEGA', 'RHO', 'SIGMA']
9 ['ALFA', 'BETA', 'DELTA', 'GAMMA', 'KAPPA', 'LAMBDA', 'OMEGA', 'RHO', 'SIGMA']
```

OMEGA leaves in 2025 and returns in 2026; the set counts it only once. An honest backtest from 2024 to 2026 needs all
nine names, with the dates on which each one was in the index.

3. With the lesson's `precios` dictionary, create a dictionary with the value of a portfolio holding 10 shares of each
   ticker, and say which ticker has the largest value.

**Solution.**

```python
diez = {t: round(10 * p, 2) for t, p in precios.items()}
print(diez)
mayor = max(diez, key=diez.get)
print(mayor, diez[mayor])
```
```output
{'SINT': 1422.2, 'ORO': 1714.4, 'RFIJ': 1022.9, 'TECH': 778.2}
ORO 1714.4
```

`round` removes the tail of decimals that floating point leaves in `10 * 102.29` (lesson 0.3.1). `max` goes through
the dictionary's keys; `key=diez.get` tells it to compare them by their value and not by their name. Lesson 0.3.5
explains what that `key` argument is and how to use it with your own functions.

## Self-assessment

1. `cierres = [139.32, 139.23, 139.56]` and then `copia = cierres; copia.append(139.98)`. How many elements does `cierres` have?

   - A. 3, because the copy was modified
   - B. 4, because `copia` and `cierres` are the same list
   - C. It raises an error
   - D. 1

   *Answer B: 4, because `copia` and `cierres` are the same list.* `copia = cierres` copies nothing: it creates another name for the same list. For an independent copy you need `cierres.copy()` or `list(cierres)`.

2. Which structure is right for storing the shares you hold of each ticker and looking them up by name?

   - A. A dictionary
   - B. A list
   - C. A tuple
   - D. A set

   *Answer A: A dictionary.* A dictionary maps each key (the ticker) to a value (the shares), and lookup by key is immediate. A list would force you to go through all of it.

3. `antes = {"A", "B", "C"}` and `ahora = {"B", "C", "D"}`. What does `antes - ahora` return?

   - A. `{"D"}`
   - B. `{"B", "C"}`
   - C. `{"A"}`
   - D. `{"A", "D"}`

   *Answer C: `{"A"}`.* The difference `antes - ahora` is the elements that were there and no longer are: the deletions from the index. The additions would be `ahora - antes`, and `^` would give both changes together.

4. A backtest uses only the companies in an index today to simulate the last ten years. What bias does it have?

   - A. Look-ahead bias
   - B. None, if the prices are adjusted
   - C. Cost bias
   - D. Survivorship bias: the companies that left the index are missing, many of them because they fell

   *Answer D: Survivorship bias: the companies that left the index are missing, many of them because they fell.* The companies in the index today are the ones that survived. Those that went bankrupt or fell far enough to leave do not appear, and the backtest comes out better than it would have been. You need the universe at each date.

5. What does `[c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])]` do?

   - A. Computes the total return
   - B. Computes each day's return relative to the previous one
   - C. Sorts the closes
   - D. Raises an error because the lists have different lengths

   *Answer B: Computes each day's return relative to the previous one.* `zip` pairs each close with the next one (`zip` stops at the shortest list) and the comprehension computes `c1 / c0 - 1` for each pair: one return per day, one fewer than the closes.

## Key points

- List: ordered, modifiable sequence (closes, watchlist). Tuple: fixed sequence, ideal for a record such as an OHLC bar.
- Assigning a list to another name does not copy it: both names point to the same list. To copy, `.copy()`.
- Dictionary: key → value (ticker → shares). `d[clave]` fails if the key does not exist; `d.get(clave, 0)` returns a default value.
- Set: unique elements with no order. The difference between the universes at two dates gives the additions and deletions; ignoring the deletions is survivorship bias.
- A comprehension builds a collection in one line: `[c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])]` gives the daily returns.

## Resources

### Research

- [Alpha Factors and Alpha Trading in Python](https://pythonparatrading.com/en/alpha-factors-and-alpha-trading-in-python/) · Research-0007 · 5 min

### Documentation

- [Python: data structures (official tutorial)](https://docs.python.org/3/tutorial/datastructures.html) · docs.python.org
- [Python: sequence, mapping and set types (official documentation)](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) · docs.python.org

### Data

- [Level 0 data: daily prices of the fictional ETF SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Download · sint_diario.csv

Previous lesson: [0.3.2 · Strings, f-strings and formatting numbers and dates](https://pythonparatrading.com/en/python-trading-course/level-0/python-strings-f-strings-formatting/)

Next lesson: [0.3.4 · Control flow: if, elif, else, for, while and match-case](https://pythonparatrading.com/en/python-trading-course/level-0/python-control-flow-if-for-while-match/)
