Watch the video above, then read on for the full technical detail and practitioner context.
What immutability means and why it matters
String immutability in Python is one of those design choices that either confuses you at first or feels obvious once you understand it. Strings in Python are immutable: once created, they cannot be changed. This shapes how you work with text every day, whether you are cleaning reference data, normalising identifiers, or validating regulatory descriptions.
Immutability forces you to be explicit about what you are doing, and makes code review safer: you see every transformation as a new variable. When a function receives a string and transforms it, the original string in the caller's scope is guaranteed unchanged. No hidden state, no surprises when debugging three functions down the call stack.
In treasury and regulatory reporting, you spend a lot of time transforming text: normalising identifiers, sanitising transaction descriptions, validating regulatory fields. Immutability means you cannot accidentally corrupt a string that another part of your code is still using. Every transformation creates a new string, leaving the original untouched.
Strings cannot change: what this really means
Consider this:
identifier = "GB00B0130H42"
identifier[0] = "U" # This will fail
Python will raise a TypeError: 'str' object does not support item assignment. You cannot overwrite a character in a string by index. The string object itself is locked against modification.
This is different from a list, where you can do this:
codes = ["GB00B0130H42", "IE0002374239"]
codes[0] = "US0378331005" # This works fine
print(codes) # ['US0378331005', 'IE0002374239']
Lists are mutable. Strings are not. That distinction shapes how you work with text in Python.
The reason is efficiency and safety. Strings are often used as dictionary keys, passed between functions, stored in sets. If strings could change, Python would have to copy them constantly to protect against accidental modification. Instead, Python makes the rule simple: strings never change. If you need a different string, create a new one.
Creating new strings: concatenation and slicing
When you need to transform a string, you create a new one. The most basic way is concatenation using the + operator:
trade_ref = "TRADE"
date_part = "20240115"
full_ref = trade_ref + "/" + date_part
print(full_ref) # TRADE/20240115
Concatenation is straightforward but can become cumbersome if you are building a string from many parts. For larger compositions, use join():
parts = ["TRADE", "20240115", "GBP", "1000000"]
full_ref = "/".join(parts)
print(full_ref) # TRADE/20240115/GBP/1000000
Slicing lets you extract a substring without modifying the original:
isin = "GB00B0130H42"
country_code = isin[0:2]
print(country_code) # GB
print(isin) # GB00B0130H42 (unchanged)
Notice that slicing creates a new string. You get back the slice. The original remains exactly as it was.
You can also use negative indices to count from the end:
isin = "GB00B0130H42"
without_checksum = isin[:-1]
print(without_checksum) # GB00B0130H4
This is useful when you need to work with the body of an identifier and ignore a trailing checksum or control character.
String methods that transform: replace, strip, upper, lower, split, join
Python provides methods that look like they modify a string, but they actually return a new string. You assign the result back to a variable or use it directly.
replace()
replace() finds a substring and returns a new string with that substring swapped out:
description = "PAYMENT PAYMENT FOR INVOCE 12345"
cleaned = description.replace("PAYMENT", "PMT")
print(cleaned) # PMT PMT FOR INVOCE 12345
print(description) # PAYMENT PAYMENT FOR INVOCE 12345 (original unchanged)
You can limit the number of replacements with a third argument:
description = "PAYMENT PAYMENT FOR INVOICE 12345"
cleaned = description.replace("PAYMENT", "PMT", 1)
print(cleaned) # PMT PAYMENT FOR INVOICE 12345
strip()
strip() removes whitespace (or specified characters) from the beginning and end of a string:
ref_with_spaces = " GB00B0130H42 "
cleaned = ref_with_spaces.strip()
print(f"'{cleaned}'") # 'GB00B0130H42'
This is vital when you are loading reference data from flat files or spreadsheets. Trailing spaces cause validation failures: a space in an ISIN will not match the database record.
lstrip() and rstrip() remove from the left or right only:
amount_text = " 1,000,000.50"
cleaned = amount_text.lstrip()
print(cleaned) # 1,000,000.50
upper() and lower()
A weekly note on treasury, liquidity and practical Python. No spam, unsubscribe any time.
These return a new string with all characters converted to uppercase or lowercase:
currency = "gbp"
standardised = currency.upper()
print(standardised) # GBP
print(currency) # gbp (original unchanged)
split()
split() breaks a string into a list of substrings:
trade_line = "TRADE/20240115/GBP/1000000/FIXED"
parts = trade_line.split("/")
print(parts)
# ['TRADE', '20240115', 'GBP', '1000000', 'FIXED']
Split and join are often used together to normalise delimiters:
messy_ref = "TRADE;20240115;GBP;1000000"
parts = messy_ref.split(";")
clean_ref = "/".join(parts)
print(clean_ref) # TRADE/20240115/GBP/1000000
join()
We touched on this earlier. join() is the inverse of split(). It takes an iterable of strings and stitches them together with a separator:
fields = ["GB", "00", "B013", "0H42"]
isin = "".join(fields)
print(isin) # GB00B0130H42
Use join() instead of repeated concatenation with +. It is faster and cleaner, especially when building strings from many parts.
When transforming strings in a pipeline, always assign or pass the result: the original is never modified by string methods.
Real scenarios: cleaning identifiers, standardising descriptions, removing noise
Let us work through three scenarios common in treasury and reporting work.
Scenario 1: Cleaning and normalising an ISIN
You receive ISINs from a data feed, but they are often mixed case and may have trailing whitespace:
raw_isins = [" gb00b0130h42 ", "IE0002374239", " US0378331005 "]
cleaned_isins = []
for raw in raw_isins:
normalized = raw.strip().upper()
cleaned_isins.append(normalized)
print(cleaned_isins)
# ['GB00B0130H42', 'IE0002374239', 'US0378331005']
Or more concisely, using a list comprehension:
raw_isins = [" gb00b0130h42 ", "IE0002374239", " US0378331005 "]
cleaned_isins = [isin.strip().upper() for isin in raw_isins]
print(cleaned_isins)
# ['GB00B0130H42', 'IE0002374239', 'US0378331005']
Scenario 2: Extracting and standardising transaction descriptions
You have transaction descriptions that include noise (extra spaces, abbreviations, inconsistent formatting) and need to extract the essential information:
description = " PAYMENT TO SUPPLIER ABC CORP "
# Remove extra spaces and standardise
words = description.split()
cleaned = " ".join(words)
print(cleaned) # PAYMENT TO SUPPLIER ABC CORP
Or remove specific known noise patterns:
description = "PAYMENT TO SUPPLIER ABC CORP RE: INVOICE #123456"
# Remove the invoice reference
cleaned = description.split("RE:")[0].strip()
print(cleaned) # PAYMENT TO SUPPLIER ABC CORP
Scenario 3: Extracting components from a structured identifier
You have a concatenated identifier and need to parse it into its components:
record = "TRADE20240115GBPJPM1000000FIXED"
# Format: type(5) + date(8) + currency(3) + bank(3) + amount(7) + rate(5)
# This assumes a fixed format that you control. In production, validate the record length first to avoid silent truncation.
trade_type = record[0:5]
trade_date = record[5:13]
currency = record[13:16]
counterparty = record[16:19]
amount = record[19:26]
rate_type = record[26:31]
print(f"Type: {trade_type}, Date: {trade_date}, Currency: {currency}")
# Type: TRADE, Date: 20240115, Currency: GBP
Combining slicing with operations to reshape text
The most practical text transformations chain multiple methods together. Here is a worked example: cleaning a transaction narrative that contains multiple delimiters and extra spaces:
raw_narrative = "SWIFT;;PAYMENT / TO / SUPPLIER XYZ CORP // REF: INV123"
# Step 1 Replace double delimiters with single
step1 = raw_narrative.replace(";;", ";").replace("//", "/")
print(step1)
# SWIFT;PAYMENT / TO / SUPPLIER XYZ CORP / REF: INV123
# Step 2 Remove extra spaces
step2 = " ".join(step1.split())
print(step2)
# SWIFT;PAYMENT / TO / SUPPLIER XYZ CORP / REF: INV123
# Step 3 Extract the payment instruction (between PAYMENT and REF)
parts = step2.split(" / ")
# parts[0] = 'SWIFT;PAYMENT'
# parts[1] = 'TO'
# parts[2] = 'SUPPLIER XYZ CORP'
# parts[3] = 'REF: INV123'
payment_target = parts[2]
print(payment_target) # SUPPLIER XYZ CORP
Each step leaves the original unchanged and builds a new string. This makes it safe to debug: you can print the result of each step to see where the transformation went.
Why immutability makes your code safer and more predictable
Consider this scenario: two functions need to work with the same string:
def format_for_report(ref):
return ref.upper()
def validate_ref(ref):
if len(ref) == 12:
return True
return False
ref = "gb00b0130h42"
formatted = format_for_report(ref)
is_valid = validate_ref(ref)
print(f"Original: {ref}, Formatted: {formatted}, Valid: {is_valid}")
# Original: gb00b0130h42, Formatted: GB00B0130H42, Valid: True
The format_for_report() function cannot change the original ref string. If you pass ref to another function later, you still have the original. This is guaranteed by immutability.
If strings were mutable, you would need to be defensive about copying strings before passing them to functions, or carefully documenting which functions modify their inputs. Immutability removes that cognitive load.
In regulatory reporting, where multiple systems process the same identifiers and amounts, this safety is invaluable. A transformation in one module cannot corrupt data elsewhere.
Practical takeaway
Strings are immutable, and that is a feature, not a limitation. Learn to think in terms of creating new strings rather than modifying old ones. Master split() and join(), lean on replace() and strip() for common cleaning tasks, and use slicing to extract and reshape text safely.
The moment you stop fighting immutability and start using it as a tool, your text handling code becomes clearer, more testable, and less prone to subtle bugs. That matters whether you are cleaning reference data in a morning batch run or validating transaction descriptions in a real time feed.
Next: f-strings and formatting for financial reporting.

The Complete Python Course
Welcome to the most practical and beginner friendly Python Bootcamp Course on YouTube.
Take the courseGet the next one in your inbox
A weekly note across Finance & Treasury, Innovation & Automation and Career Development. No spam, unsubscribe any time.
Notes across finance and treasury, innovation and automation, and career development, written by practitioners who do the work.