Lesson 0.3.2 · 45 min
Strings, f-strings and formatting numbers and dates
A ticker, a date and a line of a report are text. The lesson teaches how to split symbols such as “ES=F” or “EURUSD”, how to format prices, percentages and P&L with f-strings and how to write and read dates without ambiguity.
- Module
- 0.3 · Language fundamentals
- Duration
- 45 min
- Prerequisites
- 0.3.1 · Variables, types and operators; floating point and decimal for prices and ticks
- Author
- Jesús Cuesta
- Updated
Level 0 lessons
0.1 · Welcome and expectations
0.2 · Modern working environment
- 0.2.1 Installing Python with uv: versions and support cycle
- 0.2.2 Virtual environments and dependencies: uv init, uv add, pyproject.toml and lockfiles
- 0.2.3 Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Basic terminal, paths, environment variables and .env files
- 0.2.5 Git and GitHub: commits, branches, pull requests and .gitignore
- 0.2.6 Introduction to AI coding assistants: prompts, review and tests
0.3 · Language fundamentals
- 0.3.1 Variables, types and operators; floating point and decimal for prices and ticks
- 0.3.2 Strings, f-strings and formatting numbers and dates
- 0.3.3 Lists, tuples, dictionaries, sets and comprehensions
- 0.3.4 Control flow: if, elif, else, for, while and match-case
- 0.3.5 Functions: arguments, *args and **kwargs, lambdas and scope
- 0.3.6 Exceptions; modules and packages, with Python 3.15 lazy imports
0.4 · Intermediate Python
- 0.4.1 Object-oriented programming: classes, inheritance, composition and dataclasses
- 0.4.2 Typing: type hints, mypy or ty, and Pydantic for configurations
- 0.4.3 Iterators, generators, decorators and context managers
- 0.4.4 Files (CSV, JSON, YAML, TOML) and dates with time zones
- 0.4.5 Professional logging and testing with pytest
- 0.4.6 Concurrency: threading, multiprocessing, asyncio and free-threading
0.5 · Numerical computing and data
- 0.5.1 NumPy 2: arrays, broadcasting, vectorization, linear algebra and random generators
- 0.5.2 pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()
- 0.5.3 Selection, filtering, groupby, merge, pivot, resample and moving windows
- 0.5.4 Polars: lazy versus eager API, expressions, performance and when to pick it over pandas
- 0.5.5 DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet
0.6 · Visualisation
0.7 · Minimum mathematics and statistics
Strings: the text of a trading program
Tickers, market names, order sides, lines of a file and messages in a report: all of that is text, and in Python text
is the str type (lesson 0.3.1). A string is written between single or double quotes, which are equivalent; triple
quotes ("""…""") allow several lines.
A string is a sequence of characters in order, and each character has a position, its index, which starts at 0.
With square brackets you read one character (s[0]) or a piece (s[inicio:fin], the slice, which includes
inicio and excludes fin). Negative indexes count from the end: s[-1] is the last character.
par = "EURUSD"
print(len(par), par[0], par[-1])
print(par[:3], par[3:])
print(par[::-1])
print("USD" in par, "JPY" in par)6 E D
EUR USD
DSURUE
True Falselen gives the number of characters. [:3] takes the first three (from 0 to 2) and [3:], from index 3 to the end:
the two currencies of the pair. A third number in the slice is the step; [::-1] walks the string backwards. The in
operator asks whether a piece appears inside the string and returns a bool.
Strings are immutable: you cannot change a character (par[0] = "G" raises an error). All the operations that
seem to modify them return a new string. With + they are concatenated and with * repeated:
base, cotizada = "EUR", "USD"
simbolo = base + "/" + cotizada
print(simbolo)
print("-" * 20)EUR/USD
--------------------The first line assigns two variables at once; the form is explained with tuples in lesson 0.3.3.
Methods to clean and split symbols
Symbols arrive written in many ways: in lowercase from a form, with spaces from a spreadsheet, with suffixes that each
data provider uses in its own way. In Yahoo Finance, for example, ES=F is the E-mini S&P 500 future (the contract
nearest to expiry) and EURUSD=X, the euro-dollar rate; at other providers the same pair is EURUSD or EUR/USD, and
a cryptocurrency can be BTC-USD or BTCUSDT1Note 1 · Yahoo Finance: ES=F and EURUSD=X.
Each provider documents its symbols; when you switch providers, you have to translate them..
String methods are functions called with a dot after the value (simbolo.upper()). The ones used most for this
job:
| Method | What it does | Example | Result |
|---|---|---|---|
strip() | Removes spaces at the start and end | " es=f ".strip() | "es=f" |
upper() / lower() | Uppercase / lowercase | "es=f".upper() | "ES=F" |
partition(sep) | Splits in three at the first occurrence of sep | "ES=F".partition("=") | ("ES", "=", "F") |
split(sep) | Splits at every occurrence of sep | "BTC-USD".split("-") | ["BTC", "USD"] |
replace(a, b) | Replaces a with b | "EUR/USD".replace("/", "") | "EURUSD" |
startswith / endswith | Does it start or end with…? | "ES=F".endswith("=F") | True |
removesuffix / removeprefix | Removes a suffix or prefix if present | "EURUSD=X".removesuffix("=X") | "EURUSD" |
With them, a badly written symbol is normalized in one line, and a well-written symbol is broken down into its parts:
entrada = " es=f \n"
simbolo = entrada.strip().upper()
print(repr(entrada), "->", repr(simbolo))
raiz, sep, tipo = simbolo.partition("=")
print(raiz, tipo, simbolo.endswith("=F"))
base, cotizada = "BTC-USD".split("-")
print(base, cotizada)
print("EURUSD=X".removesuffix("=X"))
print("EUR/USD".replace("/", ""))' es=f \n' -> 'ES=F'
ES F True
BTC USD
EURUSD
EURUSDrepr shows the string as it is, with quotes and with the line break written as \n; print would show it without
them and the spaces would go unnoticed. It is the way to see what is really inside a text that does not behave as
expected.
partition always returns three pieces, even if it does not find the separator (the last two are empty); split
returns as many as there are, and assigning them to two variables only works if there are exactly two. That is why,
when a symbol has a single marker (= in Yahoo), partition is the robust option.
Splitting follows the same pattern for any provider: clean, check the format and separate the parts.
Mermaid source
flowchart TB
E[Received text] --> N[strip and upper]
N --> F{Contains =?}
F -- yes --> P[partition]
F -- no --> C[Slice by position]When the format has no separator, as in EURUSD, it is split by position: currencies always have three letters (they
are ISO 4217 codes), so [:3] and [3:] are enough. If there is neither a separator nor a fixed length (BTCUSDT),
you need a table with the symbols published by the market itself; guessing the parts is a sure source of errors.
f-strings: text with values inside
An f-string is a string with an f in front. Inside, everything between braces is evaluated as code and replaced
by its value. It is the way to build messages, file names and report lines:
ticker = "SINT"
cierre = 142.22
anterior = 139.98
print(f"{ticker} closes at {cierre}")
print(f"Change: {cierre - anterior}")SINT closes at 142.22
Change: 2.240000000000009Any expression can go inside the braces, such as the subtraction on the second line, which shows once more the floating
point of lesson 0.3.1: the text needs a format that fixes the decimals. If you forget the f, the braces appear as
they are in the text and no error warns you. A useful shortcut: an = at the end of the expression also writes its
name.
print(f"{cierre=} {anterior=}")
print(f"{cierre - anterior = :.2f}")cierre=142.22 anterior=139.98
cierre - anterior = 2.24{cierre=} writes the variable's name, an equals sign and its value: useful for following a calculation without
writing the name twice. The second line adds a format after the colon, .2f, which is what comes next.
The format mini-language
What goes after the colon inside the braces is the format specification. Its fields come in a fixed order, and each
one is optional. The header figure breaks down {pnl:>+12,.2f}, the typical format of a P&L column:
| Field | Example | What it does |
|---|---|---|
| Alignment | < > ^ | Left, right, centered (with a fill character in front, if wanted: *>) |
| Sign | + | Writes the sign on positive numbers too (by default, only on negative ones) |
| Width | 12 | Takes up at least 12 characters, padding with spaces |
| Grouping | , or _ | Separates thousands with a comma or an underscore |
| Precision | .2 | Number of decimals (with f and %) |
| Type | f % e d | Fixed-point decimal, percentage (multiplies by 100), scientific notation, integer |
pnl = 1426.71
rent = 0.07041
volumen = 2_322_609
print(f"{cierre:.2f} {cierre:.4f} {cierre:.0f}")
print(f"{rent:.2%} {rent:+.1%} {-rent:+.1%}")
print(f"{volumen:,} {volumen:_} {volumen:.2e}")
print(f"{pnl:+,.2f} {-pnl:+,.2f}")
print(f"[{pnl:>12,.2f}] [{pnl:<12,.2f}] [{'SINT':^10}]")142.22 142.2200 142
7.04% +7.0% -7.0%
2,322,609 2_322_609 2.32e+06
+1,426.71 -1,426.71
[ 1,426.71] [1,426.71 ] [ SINT ]Three details worth settling. The % type multiplies by 100: the return is stored as a fraction of one (0.07041) and
displayed as a percentage, never the other way around. The .2f format rounds only what is displayed: the variable
keeps the same value, with all its decimals. And the format's rounding is the floating-point one, with the same
misleading ties as round() in lesson 0.3.1; for amounts that have to reconcile to the cent, the rounding is done
beforehand with Decimal.
The explicit sign (+) is the convention of P&L reports and price changes: +1,426.71 and -1,426.71 can be told
apart at a glance, and a column of signed figures that always has the same width is easier to read.
An aligned trade report
Width and alignment are used to build text tables: the log of a script that trades, an email with the day's summary or the output of a backtest in the terminal. Numbers are right-aligned, so that the units line up one below the other, and text is left-aligned.
The five trades in the cell are the first ones in operaciones_sint.csv (link in “Resources”), with the P&L already net
of commissions. The list of tuples and the for loop are explained in lessons 0.3.3 and 0.3.4; here it is enough to
know that the line inside the loop is repeated for each trade:
operaciones = [
("2021-03-04", "ORO", "corto", 200, 190.08, -3123.56),
("2021-09-02", "SINT", "largo", 180, 108.64, 7.43),
("2021-10-27", "SINT", "corto", 10, 112.07, -31.50),
("2021-11-10", "SINT", "corto", 40, 104.30, -306.72),
("2022-07-22", "SINT", "largo", 200, 102.83, 1426.71),
]
cabecera = (f"{'Date':<10} {'Symbol':<7} {'Side':<5} "
f"{'Qty':>5} {'Entry':>8} {'P&L':>10}")
print(cabecera)
print("-" * len(cabecera))
for fecha, simbolo, lado, cant, entrada, pnl in operaciones:
print(f"{fecha:<10} {simbolo:<7} {lado:<5} "
f"{cant:>5} {entrada:>8.2f} {pnl:>+10,.2f}")Date Symbol Side Qty Entry P&L
-------------------------------------------------------
2021-03-04 ORO corto 200 190.08 -3,123.56
2021-09-02 SINT largo 180 108.64 +7.43
2021-10-27 SINT corto 10 112.07 -31.50
2021-11-10 SINT corto 40 104.30 -306.72
2022-07-22 SINT largo 200 102.83 +1,426.71The header uses the same widths as the rows, and the dashed line is as long as the header thanks to len. Two
f-strings in a row inside parentheses are joined into a single string: it is the way to split a long line without the
code going over 72 characters. The strings inside the braces ({'Date':<10}) use single quotes to stand apart from the
f-string's double quotes; since Python 3.12 the double quotes could be repeated, but it reads worse.
Spanish format without touching the locale
Python formats numbers the English way: comma for thousands and point for decimals. In Spain it is written the other
way around, 1.426,71, and the percent sign is separated from the figure (7,04 %). For a report that a person reads,
the local format matters: 1,426 is one thousand four hundred twenty-six for an English reader and one point four for
a Spanish one.
Python has an “official” way to do it, the locale module and the n format type, which use the system's regional
settings. It has two problems. The result depends on the machine: the same script writes 1.426,71 on a laptop set to
Spanish and 1426.71 on a server with the C locale, the usual one in containers. And locale.setlocale changes a
global setting of the process, which affects everything that runs in it, libraries included.
The robust alternative is to format the English way, which is deterministic, and then swap the two signs.
str.maketrans(",.", ".,") creates a translation table (each comma for a point and each point for a comma) and
translate applies it in a single pass. The cell wraps it in a function so as not to repeat it; functions are covered
in lesson 0.3.5, and here it is enough to know that def gives a name to a calculation and return returns its result:
A_ESPANOL = str.maketrans(",.", ".,")
def es(numero, decimales=2):
"""Number with points for thousands and a comma for decimals."""
return f"{numero:,.{decimales}f}".translate(A_ESPANOL)
def es_pct(tanto_por_uno, decimales=2):
"""Percentage in Spanish format: 7,04 %."""
return es(tanto_por_uno * 100, decimales) + " %"
print(es(1426.71), es(-3123.56), es(2_322_609, 0))
print(es_pct(0.07041), es_pct(-0.25, 1))
print(f"Day's P&L: {es(pnl)} €")1.426,71 -3.123,56 2.322.609
7,04 % -25,0 %
Day's P&L: 1.426,71 €The precision can also come from a variable: {numero:,.{decimales}f} nests a pair of braces inside the
specification. The result is the same on any machine, and the numbers are always stored as numbers: the Spanish format
is only for displaying them. A data file (CSV, Parquet) never contains formatted numbers, neither in Spanish nor in
English format.
Dates: ISO 8601, strftime and strptime
A date written as text is ambiguous: 03/04/2025 is April 3 in Spain and March 4 in the United States. The ISO
8601 standard solves the problem by writing from the largest unit to the smallest, 2025-04-03, with only one
possible order. It is the format of the course's data files, of brokers' APIs and of pandas. It has one more advantage:
as text, ISO dates sort the same way as on the calendar.
iso = ["2025-04-03", "2024-12-31", "2025-01-15"]
espanol = ["03/04/2025", "31/12/2024", "15/01/2025"]
print(sorted(iso))
print(sorted(espanol))['2024-12-31', '2025-01-15', '2025-04-03']
['03/04/2025', '15/01/2025', '31/12/2024']sorted sorts alphabetically. With ISO 8601 the result is chronological; with the Spanish format, December 31, 2024
ends up last because “31” comes after “15” in alphabetical order.
To operate with dates (add days, find the day of the week) you do not use the text but the date type from the
datetime module. date.fromisoformat turns an ISO text into a date, and isoformat does the opposite. For any other
format, strftime (“string from time”) writes a date with a pattern, and strptime (“string parse time”) reads it:
| Code | Meaning | Example |
|---|---|---|
%Y | Four-digit year | 2025 |
%m | Two-digit month | 12 |
%d | Two-digit day | 31 |
%H:%M:%S | Hours, minutes and seconds | 15:30:00 |
%A / %B | Name of the day / of the month (depends on the locale) | Wednesday / December |
from datetime import date, datetime
fecha = date.fromisoformat("2025-12-31")
print(fecha, fecha.year, fecha.month, fecha.day)
print(fecha.strftime("%d/%m/%Y"), f"{fecha:%d/%m/%Y}")
print(fecha.weekday(), f"{fecha:%A}")
leida = datetime.strptime("31/12/2025", "%d/%m/%Y").date()
print(leida == fecha, leida.isoformat())
orden = datetime.fromisoformat("2025-12-31T15:30:00")
print(orden, orden.date(), orden.time())2025-12-31 2025 12 31
31/12/2025 31/12/2025
2 Wednesday
True 2025-12-31
2025-12-31 15:30:00 2025-12-31 15:30:00f-strings accept the same codes after the colon: {fecha:%d/%m/%Y} is equivalent to strftime. weekday numbers the
days from Monday (0) to Sunday (6): December 31, 2025 is a Wednesday. datetime adds the time to the date; in ISO
8601, the T separates the two parts.
The names of days and months (%A, %B) have the same problem as the number format: they depend on the locale and,
on the course runner, as on many servers, they come out in English. The solution is the same: do not depend on it.
DIAS = ("lunes", "martes", "miércoles", "jueves",
"viernes", "sábado", "domingo")
MESES = ("enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre",
"diciembre")
print(f"{DIAS[fecha.weekday()]}, {fecha.day} de "
f"{MESES[fecha.month - 1]} de {fecha.year}")
print(f"Report_{fecha:%Y%m%d}.txt")miércoles, 31 de diciembre de 2025
Report_20251231.txtDIAS[fecha.weekday()] uses the day's number as an index, just as with the characters of a string. The months have 1
subtracted because month goes from 1 to 12 and indexes start at 0. The last line shows another use of ISO 8601: file
names with the date as YYYYMMDD appear sorted in any folder.
Exercises
- Write the code that, starting from
" usdjpy ", gets the normalized symbolUSDJPY, the base currency and the quote currency, and displays it asUSD/JPY.
Solution
texto = " usdjpy "
par = texto.strip().upper()
base, cotizada = par[:3], par[3:]
print(par, base, cotizada)
print(f"{base}/{cotizada}")USDJPY USD JPY
USD/JPYstrip removes the spaces, upper converts to uppercase and the slices [:3] and [3:] separate the two
currencies, which always have three letters.
- With
pnl = -1288.04andrent = -0.10063, write a line that looks exactly like this (width 12 for the P&L and 8 for the return, right-aligned):P&L: -1,288.04 | Ret.: -10.06%.
Solution
pnl = -1288.04
rent = -0.10063
print(f"P&L: {pnl:>12,.2f} | Ret.: {rent:>8.2%}")P&L: -1,288.04 | Ret.: -10.06%>12 reserves twelve characters and aligns to the right; , separates the thousands and .2f keeps two decimals. In
the return, .2% multiplies by 100 and adds the symbol: “-10.06%” takes up seven characters, so width 8 leaves one
space in front.
- A broker's file contains the date
"04/03/2025 09:30"in US format (month/day/year). Convert it to ISO 8601 and write the day of the week in Spanish.
Solution
momento = datetime.strptime("04/03/2025 09:30", "%m/%d/%Y %H:%M")
print(momento.isoformat())
print(DIAS[momento.weekday()])2025-04-03T09:30:00
juevesThe strptime pattern has to describe the text as it arrives: %m/%d and not %d/%m. With the Spanish pattern, the
date would have been read as March 4 without any error: it is the most dangerous failure, because it gives no warning.
Footnotes
Each answer is folded under its question
Self-assessment
What does
"EURUSD"[3:]return?- A
"EUR" - B
"USD" - C
"RUSD" - D
"URUSD"
Show answer
Answer B.
"USD"Indexes start at 0, and the slice
[3:]goes from the character at index 3 to the end:U,S,D.[:3]would giveEUR.- A
What does
f"{-1288.04:+,.2f}"display?- A
-1288.04 - B
-1.288,04 - C
-1,288.04 - D
+1,288.04
Show answer
Answer C.
-1,288.04+asks for the sign always (here the number is already negative),,groups thousands with commas and.2fkeeps two decimals. This is the English format: the comma separates thousands and the point, decimals.- A
Why do data files store dates as
2025-12-31and not as31/12/2025?- ABecause Python cannot read the Spanish format
- BBecause ISO 8601 is not ambiguous and, as text, it sorts the same way as on the calendar
- CBecause it takes up less space
- DBecause pandas requires it
Show answer
Answer B. Because ISO 8601 is not ambiguous and, as text, it sorts the same way as on the calendar
03/04/2025is April 3 in Spain and March 4 in the United States;2025-04-03has only one reading. And since it goes from the largest unit (year) to the smallest (day), alphabetical order matches chronological order.A script formats amounts with
f"{x:n}"so that they come out in the system's format. What is the problem?- ANone: it is the recommended way
- BThat
ndoes not accept decimals - CThat the result depends on each machine's
locale: the same script writes different reports on the laptop and on the server - DThat it is slower
Show answer
Answer C. That the result depends on each machine's
locale: the same script writes different reports on the laptop and on the servernuses the active regional settings, which change between machines and on many servers areC(no thousands separator). A report has to come out the same everywhere: better to fix the format in the code.
Key points
- A string is an immutable sequence of characters: it is indexed from 0, sliced with
[inicio:fin]and its methods return new strings. - To split symbols,
partitionandsplitseparate on a character andremovesuffixremoves a known suffix. Normalize withstripandupperbefore comparing. - The format goes after the colon in the f-string:
{pnl:>+12,.2f}is right-aligned, with sign, width 12, thousands with commas and two decimals. - The Spanish format is obtained by swapping commas and points with
translate, without touching thelocale: that way the report comes out the same on any machine. - Dates are stored in ISO 8601 (
2025-12-31): they are not ambiguous and sort correctly as text.strftimeandstrptimeconvert betweendateand any other format.