s0 = ""
print(s0)
Programming for Engineers
Python’s built-in strings (str) are
immutable and store Unicode text.
Indexing (s[i]) returns a new 1-character
str, not a separate “char” type. CPython (underlying
implementation) stores the string in the smallest possible internal
“kind” (1/2/4 bytes per code point). Manipulating ASCII-only strings is
often faster than “full Unicode” strings. Immutability
means: - assignment to an existing string is not possible - any
operation that “changes” a string (concatenation, replace, upper,
slicing, etc.) creates a new str object
and copies data - the original reference (variable) will (usually)
“point” to the original string and not the new one Slicing
creates a copy (there are no “views” into another string). or
very large strings, avoid repeated slicing in loops; prefer parsing by
indices or using iterators. Some methods (in and
.find()) are optimized in CPython and are
usually faster than writing a manual loop.
Empty string:
a string a string a string a string
Multiline strings are often used as documentation strings in Python code. For example:
def mandelbrot_fractal(width=800, height=600, max_iterations=100, zoom=1.0):
"""Generate and return a Mandelbrot set fractal image.
Renders a visualization of the Mandelbrot set by iterating the formula
z = z^2 + c for each pixel coordinate. Points are colored based on how
many iterations it takes for the magnitude to exceed 2.
Args:
width (int): Image width in pixels. Default is 800.
height (int): Image height in pixels. Default is 600.
max_iterations (int): Maximum iteration count for color depth.
Higher values reveal more detail. Default is 100.
zoom (float): Zoom level; higher values magnify the view.
Default is 1.0 (no zoom).
Returns:
PIL.Image: Mandelbrot set fractal image.
"""
xmin, xmax = -2.5 / zoom, 1.0 / zoom
ymin, ymax = -1.25 / zoom, 1.25 / zoom
x = np.linspace(xmin, xmax, width)
y = np.linspace(ymin, ymax, height)
X, Y = np.meshgrid(x, y)
C = X + 1j*Y
Z = np.zeros_like(C)
M = np.zeros(C.shape)
for i in range(max_iterations):
mask = np.abs(Z) <= 2
Z[mask] = Z[mask]**2 + C[mask]
M[mask] = i
M = (M / max_iterations * 255).astype(np.uint8)
return Image.fromarray(M, mode='L')Quotation can be used within a string, as long as it is of different
style than the “outer” one that delimits the string. Alternatively, the
quotes can be also escaped with the backslash
\.
There are a few special control characters that involve a letter
“escaped” with a backslash \. Remember, since backslash is
used for control sequences, we need to use a double backslash
\\ to print a single backslash. You can also double escape
a control sequence to make it “not” a control sequence. Putting
r before the quotation marks (e.g., r"...")
makes the string “raw” - escape sequences are ignored. This can be
sometimes useful - for example, see the print with
backslash below.
print("Use \\n to print a \n new line", end=".\n")
print("Use \\r to print a \r carriage return", end=".\n")
print("Use \\t to print a \t tab", end=".\n")
print(r"Use \\ to print a \ backslash", end=".\n")
print("Use \\' to print a \' single quote", end=".\n")
print('Use \\" to print a \" double quote', end=".\n")
print("Use \\x41 (\\x<hexcode>)to print a hex character '\x41'\n\t"
" (code for letter 'A'=65; 65 to hex=0x41)", end=".\n")
print("Use \\u0041 (\\u<4 digit hexcode>)to print a unicode character '\u0041'\n\t"
" (see unicode chars below)", end=".\n")Use \n to print a
new line.
Use \r to print a carriage return.
Use \t to print a tab.
Use \\ to print a \ backslash.
Use \' to print a ' single quote.
Use \" to print a " double quote.
Use \x41 (\x<hexcode>)to print a hex character 'A'
(code for letter 'A'=65; 65 to hex=0x41).
Use \u0041 (\u<4 digit hexcode>)to print a unicode character 'A'
(see unicode chars below).
Other types can be cast to strings using the str
function.
42
[1, 2, 3]
Even “complex” objects (instances of classes) can be cast to strings:
The behavior of being cast to string for objects is controlled by the
__str__ method.
ord,
chr, hex functionsEach character in a string is actually stored (in memory) as an
integer - ASCII or Unicode code of the
character. The ord function returns the integer value of a
character. Likewise, any integer can be cast to a character using the
chr function. “Standard” ASCII characters have codes from
\(0\) to \(127\) (\(0\) to \(31\) are control characters and \(32\) to \(126\) are printable characters). The “old”
extended ASCII table (\(128\) to \(255\)) is typically not used. For “special”
characters, the Unicode table is used.
Even “non-letter” characters have their code. For example, a newline
is stored as a 10 in memory.
some text
`chr(10)`, i.e., new line was printed before this text
|> <- 'space' character
|> @ <- 'At' character
There are also non-printable control characters:
Quick tip on how to get the code of a character (without searching on the Internet):
Or you can print all characters in a range:
code: 97, char: a
code: 98, char: b
code: 99, char: c
code: 100, char: d
code: 101, char: e
code: 102, char: f
code: 103, char: g
Some unicode characters can be printed the same way as ASCII characters.
|> π <- pi
|> σ <- sigma
|> € <- euro symbol
|> ♞ <- 'knight' chess piece symbol
Alternatively, \u control sequence can be used with the
hexadecimal code (see below) of the character. The code must always be 4
digits (i.e., pad with zeros in front, if necessary).
|> π <- pi (960 in hex: 0x3c0 )
|> σ <- sigma (963 in hex: 0x3c3 )
|> € <- euro symbol (8364 in hex: 0x20ac )
|> ♞ <- 'knight' chess piece symbol (9822 in hex: 0x265e )
It is possible to print even “more complex” characters but that is beyond the scope of this subject. For those interested, here is a bit of Unicode How To.
The hex function returns the hexadecimal representation
of an integer, encoded as a string. There are similar functions for
octal (oct) and binary (bin) representations.
You can also define integers as hex (or oct and bin) numbers using the
0x<hexcode> (or 0o<octalcode> and
0b<binarycode>) prefix. For example,
0xff is 255 in hexadecimal representation.
0o127 is 87 in octal representation.
0b1000011 is 67 in binary representation.
0x79 <- hex representation of 121
0o171 <- octal representation of 121
0b1111001 <- binary representation of 121
121 <- 121 written in hex is 0x79
121 <- 121 written in oct is 0o171
121 <- 121 written in binary is 0b1111001
Caesar cipher is one of the simplest “encryptions” possible. Each
letter is shifted by a certain number of positions in the alphabet,
“wrapping around” if necessary. To implement this in Python, we can use
the ord and chr functions to convert a
character to its ASCII code and vice versa.
def caesar_cipher(text, shift):
result = []
for char in text:
if char.isalpha():
# Get Unicode code point and shift it
shifted = ord(char) + shift
# Ensure it wraps around within A-Z or a-z
if char.isupper():
shifted = (shifted - ord('A')) % 26 + ord('A')
else:
shifted = (shifted - ord('a')) % 26 + ord('a')
result.append(chr(shifted))
else:
result.append(char)
return ''.join(result)
shift = 3
encrypted = caesar_cipher("Hello, World!", shift)
print("Encrypted text:", encrypted)
decrypted = caesar_cipher(encrypted, -shift)
print("Decrypted text:", decrypted)Encrypted text: Khoor, Zruog!
Decrypted text: Hello, World!
Bit shift cipher is another possible “encryption” method. There are several ways how to implement it. Here is the simplest possible implementation in Python:
def bit_shift_cipher(text, backwards=False):
if backwards:
result = [chr(ord(char) >> 2) if char != ' ' else ' ' for char in text]
else:
result = [chr(ord(char) << 2) if char != ' ' else ' ' for char in text]
return ''.join(result)
encrypted = bit_shift_cipher("Hello, World! Do you have 123 potatoes?")
print("Encrypted text:", encrypted)
decrypted = bit_shift_cipher(encrypted, backwards=True)
print("Decrypted text:", decrypted)Encrypted text: ĠƔưưƼ° ŜƼLjưƐ ĐƼ ǤƼǔ ƠƄǘƔ ÄÈÌ ǀƼǐƄǐƼƔnjü
Decrypted text: Hello, World! Do you have 123 potatoes?
The operators << and >> are
used for bit shifting. For example, 4 << 2 is
16 (binary 0b00100 to 0b10000 -
the 1 is shifted twice to the left) and
4 >> 2 is 1 (binary 0b00100
to 0b0001 - the 1 is shifted twice to the
right). Of course, the downside of this cipher is that not all shifts
will produce printable characters for all letters. Can you think of a
way to improve it?
Strings can be simply concatenated using the +
operator.
“Other” types can be concatenated to strings after being cast to a string:
Strings can be multiplied using the * operator. The
multiplication creates a string that is the concatenation of the
original string repeated n times.
The print function is fairly simple. You can print on or
more items, separated by commas.
You can use the sep and end parameters to
change the separator and end character.
The end argument is useful if you need to change the
default print behavior - “appending” a new line to the end
of the output.
s1 = "a"
s2 = "b"
print("there will be an empty line below this", end="\n\n") # double new line
print("a text to be overwritten <- that'll be overwritten", end="\r") # carriage return
print("this text will overwrite it") # carriage return and new line
print("normal text but with a semi-colon at the end", end=";\n")there will be an empty line below this
a text to be overwritten <- that'll be overwrittenthis text will overwrite it
normal text but with a semi-colon at the end;
(The above might behave weirdly in PDF, try it in normal Python)
Where did the text from the second print go? It was overwritten by
the text from the next print, since \r (carriage return)
returns the “cursor” to the beginning of the line. Normally,
\r is used with \n (new line) to move the
cursor to the beginning of a new line.
A common task is to assemble a text from values stored in variables.
This is called string formatting (or string
interpolation in other languages). One can simply concatenate
strings and variables (cast to strings, if needed) using the
+ operator. However, this does not allow for extra
formatting (e.g., for numbers) and is slightly slower.
Let’s first define some variables we will be using:
In older Python versions % was used for string
formatting (still used in some places for legacy reasons).
A newer alternative is to use the format method of a
string:
format
methodThe format method also supports keyword arguments:
a string and a number 42 and a float number 3.14
Notice how the arguments do not need to be in order as they appear in the string. This is useful when we need to supply the value from a dictionary:
The newest and recommended approach is to use the f-strings (except when you, for example, need to get the values from a dictionary):
a string and a number 42 and a float number 3.14
The nice things about the f-strings is that the variable is “in-place” where it will appear in the string, which makes it more intuitive.
The formatting is done by providing the formatting specification
after the colon :. There is a whole bunch of formatting
options. Here we will cover only a few, more can be found here.
s - stringd - integerf - floatx - hexadecimalb - binaryIt is possible to specify the number of digits for decimals/integers (or total number of characters, including the point, for floats). This number specifies the minimum number of digits (characters), meaning, “longer” numbers are not truncated. However, “shorter” numbers will be padded (by spaces or zeros), so that the number will take at least the specified number of characters in the output. For floating point numbers, a “fixed” number of digits of the fractional part (after the decimal point) can be specified (i.e., the fractional precision).
Let’s start with decimal (integer) numbers:
|> 342 <- three digits minimum, 'nothing' happens
|> 42 <- notice the empty space before the number
|> 42 <- no padding here
|> 042 <- zero-padded number
Next, let’s look at floating point numbers. With floats, we are mostly interested in formatting of the fractional part.
|> 3.1400 <- padded by zeros to 4 digits
|> 3.1415 <- nothing happens, if there are exactly 4 fractional digits
|> 3.1416 <- rounded to 4 fractional digits
The minimum number of characters can be specified as well (digits, the decimal point and potential padding or sign).
print(f"|> {3.14159} <- no formatting")
print(f"|> {3.14159:5.3f} <- 3 fractional digits, minimum 5 characters")
print(f"|> {3.14159:6.3f} <- 3 fractional digits, minimum 6 characters")
print(f"|> {3.14159:06.3f} <- 3 fractional digits, minimum 6 characters (zero-padded)")
print(f"|> {3.14159:10f} <- at least 10 characters, space padded")
print(f"|> {3.14159:010f} <- at least 10 characters, zero-padded")|> 3.14159 <- no formatting
|> 3.142 <- 3 fractional digits, minimum 5 characters
|> 3.142 <- 3 fractional digits, minimum 6 characters
|> 03.142 <- 3 fractional digits, minimum 6 characters (zero-padded)
|> 3.141590 <- at least 10 characters, space padded
|> 003.141590 <- at least 10 characters, zero-padded
We can also control how the sign will appear. “Standard” length with space for positive numbers:
|> 3.1416 <- space for positive, minus for negative numbers
|> -3.1416 <- space for positive, minus for negative numbers
Print plus for positive and minus for negative numbers:
|> +3.1416 <- space for positive, minus for negative numbers
|> -3.1416 <- space for positive, minus for negative numbers
We can also sort of “cast” numbers to different “types”:
The number 227 as hexadecimal: e3
The number 227 as binary: 11100011
The number 0.238095 as percent (2 decimal places): 23.81%
The padding of numbers or any actually any text can be controlled as well. There are these three options (there is also ‘=’ but we will ignore it):
< : left align> : right align^ : centeringA sequence of characters can be specified before the alignment
character to specify the padding (fill) value. For example
:-^ specifies centering alignment with dashes used for
padding. Let’s use some of the examples from above (all floats are
rounded to 3 fractional digits):
# default behavior:
print(f"|> {3.14159:11.3f} <- at least 11 chars, right align, space padded")
# modified alignment or padding:
print(f"|> {3.14159:->11.3f} <- at least 11 chars, right align, padded with '-'")
print(f"|> {3.14159:<11.3f} <- at least 11 chars, right align, space padded")
print(f"|> {3.14159:-<11.3f} <- at least 11 chars, right align, padded with '-'")
print(f"|> {3.14159:^11.3f} <- at least 11 chars, center align, space padded")
print(f"|> {3.14159:-^11.3f} <- at least 11 chars, center align, padded with '-'")|> 3.142 <- at least 11 chars, right align, space padded
|> ------3.142 <- at least 11 chars, right align, padded with '-'
|> 3.142 <- at least 11 chars, right align, space padded
|> 3.142------ <- at least 11 chars, right align, padded with '-'
|> 3.142 <- at least 11 chars, center align, space padded
|> ---3.142--- <- at least 11 chars, center align, padded with '-'
Padding strings (works for “general” values). s to
specify string value can be used but it is not necessary.
print(f"|> {'hello':11} <- (default) left align, space padded")
print(f"|> {'hello':11s} <- with 's', left align (default), space padded")
print(f"|> {'hello':-<11} <- left align 'forced', padded with '-'")
print(f"|> {'hello':->11} <- right align, padded with '-'")
print(f"|> {'hello':^11} <- center align, space padded")
print(f"|> {'hello':-^11} <- center align, padded with '-'")|> hello <- (default) left align, space padded
|> hello <- with 's', left align (default), space padded
|> hello------ <- left align 'forced', padded with '-'
|> ------hello <- right align, padded with '-'
|> hello <- center align, space padded
|> ---hello--- <- center align, padded with '-'
There are a few useful methods for string manipulation.
First of, we can get the length of a string:
We can split a string into a list of substrings:
['a', 'long', 'string', 'made', 'of', 'words']
['list', 'of', 'things', 'separated', 'by', 'commas']
Alternatively, we can join a list of strings into a single string:
a long string made of words
a + long + string + made + of + words
Joining strings with join is faster than concatenating
them with +:
61.4 μs ± 3.09 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
7.12 μs ± 758 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Few concatenations (very small N):
s = a + b + c or f-strings are fine and the cleanest (most
readable code). However, each addition is evaluated separately and
allocates a new string (data is copied). Many text pieces (large
N, e.g. in a loop): list.append(...) and then
"".join(parts) is much faster. New string allocation and
copying is done in a single step. For streaming text
generation: io.StringIO() but thats out of scope
of this subject.
It is possible to convert all characters in a string to upper or lower case or capitalize just the first letter:
my_string = "This is AlLeGeDlY a Sentence."
print(my_string.upper()) # all upper case
print(my_string.lower()) # all lower case
print(my_string.capitalize()) # first letter in upper case
print(my_string.title()) # first letter of each word in upper case
print(my_string.swapcase()) # swapped case for each characterTHIS IS ALLEGEDLY A SENTENCE.
this is allegedly a sentence.
This is allegedly a sentence.
This Is Allegedly A Sentence.
tHIS IS aLlEgEdLy A sENTENCE.
It is possible to search for parts of a string - there are two
methods find and index. The behavior of the
index method is similar to the list method of
the same name. However, you can search not only for individual
characters, but also for substrings. Unlike the index
method, the find method does not throw an error when the
substring is not found. Instead, it returns -1. Searching
is case sensitive. The search returns the first occurrence of the
substring, starting from the left. if needed, the start and end indices
for searching can be specified. The find function also
allows a second parameter that controls the start of the search and a
third parameter that controls the end of the search. Most searching,
counting, etc. functions are case sensitive - lower and upper case
letters are are treated as different characters.
8
Substring not found!
my_string = "This is Some Sentence of Wisdom."
print(my_string.find("Some"))
print(my_string.find("some"))
where_is_is = my_string.find("is")
print(f"First occurrence of 'is' is at {where_is_is}")
new_search_start = where_is_is + 1
print(f"Second occurrence of 'is' is at {my_string.find('is', new_search_start)}")
print(f"Position of 'is' between index 10 and 15: {my_string.find('is', 10, 15)}")8
-1
First occurrence of 'is' is at 2
Second occurrence of 'is' is at 5
Position of 'is' between index 10 and 15: -1
Similarly to lists, occurrences of substrings can be counted using
the count method.
Substrings can be replaced in a string with the replace
method. This method “greedily” replaces all occurrences of the
substring. For example, if we want to replace every “aha” with “ou” in
the string “ahahaha”, we will get “ouhou” and not “ououou”. This
limitation affects the count method as well. I.e.,
"ahahaha".count("aha") will return 2 and not
3.
This is the Sentence of Wisdom.
This, is, Some, Sentence, of, Wisdom.
This, is, Some Sentence of Wisdom.
The functions startswith and endswith let
you check whether a string starts or ends with a substring.
True
False
True
True
True
True
The function strip removes characters from the start and
end of a string. It is often used to remove leading and trailing
white space (new lines, spaces, tabs). To remove the white
space, call it without any arguments.
this is a string.
another text
this is a string. another text
It can also be used to remove other characters:
my_string = "Captains log: We arrived at this weird place."
print(my_string.strip("Captains log: .")) # notice the dot
another_string = "*** This is a headline ***"
print(another_string.strip("*")) # remove all asterisks
print(another_string.strip("*").strip()) # remove asterisks and then white space
print(another_string.strip("* ")) # remove asterisks and space at the same timeWe arrived at this weird place
This is a headline
This is a headline
This is a headline
There are several string methods that evaluate whether the string has “some properties” - e.g., whether it contains a number, letters, etc.
print("abcdef".isalpha()) # True, because it only contains letters
print("ab1cdef".isalpha()) # False, because it contains a number
print("ab1cdef".isalnum()) # True, because it contains letters and numbers
print("ab1cdef".isnumeric()) # False, because it contains letters
print("123".isnumeric()) # True, because it only contains numbers
print("ab1cdef".isascii()) # True, because it only contains ASCII characters
print("čožzmif".isascii()) # False, because it contains non-ASCII characters
print("ABC".isupper()) # True, because it only contains uppercase letters
print("ABC".islower()) # False, because it contains uppercase letters
print("abc".islower()) # True, because it only contains lowercase letters
print("my_var".isidentifier()) # True, because it could be a valid Python identifier
print("my var".isidentifier()) # False, because it contains a space - not a valid identifierTrue
False
True
False
True
True
False
True
False
True
True
False
Many string methods have their “right-sided” version. For example,
rfind is the right-sided version of find.
Normally, find will return the first occurrence of a substring, starting
from the beginning of the string (“left” side). The rfind
will do the same but starting from the end of the string (“right”
side).
2
26
You can explore the other right-sided functions on your own (e.g.,
rindex,rsplit, rpartition, etc.).
In Python, str objects have a translate
method. This allows for efficient character-level replacement
and deletion. It is similar to replace but only individual
characters can be replaced or deleted (i.e., not a substrings). To use
translate, you need to create a translation table
(or mapping). This can be done manually, by creating a
dictionary where the keys are ordinary values of characters and the
values are characters you want to replace them with.
my_string = "You can explore the other right-sided functions on your own (e.g., `rindex`,`rsplit`, `rpartition`)."
my_mapping = {
ord("c"): "b", # replace every character "c" with "b"
ord("`"): "'", # replace "`" with "'"
32: "_", # replace spaces (ord(" ") == 32) with "_"
ord("-"): None # delete "-"
}
print(my_string)
print(my_mapping)
print(my_string.translate(my_mapping))You can explore the other right-sided functions on your own (e.g., `rindex`,`rsplit`, `rpartition`).
{99: 'b', 96: "'", 32: '_', 45: None}
You_ban_explore_the_other_rightsided_funbtions_on_your_own_(e.g.,_'rindex','rsplit',_'rpartition').
To make things easier, there is also a method
str.maketrans that allows you to create a translation table
from a string, perhaps more intuitively. The syntax is:
The method creates a mapping that will replace each character fom the first string with the corresponding character (at the same position) from the second string, and will delete each character from the third string.
Here is how we can create the same mapping as in the previous example:
You can explore the other right-sided functions on your own (e.g., `rindex`,`rsplit`, `rpartition`).
{99: 98, 96: 39, 32: 95, 45: None}
You_ban_explore_the_other_rightsided_funbtions_on_your_own_(e.g.,_'rindex','rsplit',_'rpartition').
An example of use is replacement of accented characters with their plain version (e.g., for compatibility reasons).
import os
# accented characters and their plain (ascii) version
accented_characters = "čšťžýáíéěřťďňôöüúůóČŠŤŽÝÁÍÉĚŘŇÔÖÜÚÓ"
ascii_alternatives = "cstzyaieertdnoouuuoCSTZYAIEERNOOUUO"
# load some long text
with open(os.path.join(os.getcwd(), "rur.txt")) as f:
long_text = f.read()
trans = str.maketrans(accented_characters, ascii_alternatives)
plain_text = long_text.translate(trans)
print("> Original text:")
print(long_text[:502])
print("\n> Plain text:")
print(plain_text[:502])> Original text:
Ústřední kancelář továrny Rossum s Universal Robots. Vpravo vchod. Okny
v průčelní stěně pohled na nekonečné řady továrních budov. Vlevo další
ředitelské místnosti.
Domin: (sedí u velikého amerického psacího stolu v otáčecím křesle. Na
stole žárovka, telefon, těžítka, pořadač dopisů, atd., na stěně vlevo
veliké mapy s lodními a železničními liniemi, veliký kalendář, hodiny,
jež ukazují něco málo před polednem; na stěně vpravo tištěné plakáty:
"Nejlacinější práce: Rossumovi Roboti"
> Plain text:
Ustredni kancelar tovarny Rossum s Universal Robots. Vpravo vchod. Okny
v prucelni stene pohled na nekonecne rady tovarnich budov. Vlevo dalsi
reditelske mistnosti.
Domin: (sedi u velikeho americkeho psaciho stolu v otacecim kresle. Na
stole zarovka, telefon, tezitka, poradac dopisu, atd., na stene vlevo
velike mapy s lodnimi a zeleznicnimi liniemi, veliky kalendar, hodiny,
jez ukazuji neco malo pred polednem; na stene vpravo tistene plakaty:
"Nejlacinejsi prace: Rossumovi Roboti"
Replacing accented letters in a text with 6476 characters...
700 μs ± 13.7 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Another useful example might be “sanitizing” file names - removing characters that are not allowed in file names (e.g., spaces, slashes, etc.)
String elements can be accessed the same way as lists - using square
brackets [] with an index. Indexing has the same mechanism
as in Python lists.
a
s
a lon
ade of words
gsrn aeo o
It is often useful to loop through strings with a for-loop. This is the same as looping through a list (or rather, a tuple, since we cannot assign to a string item).
char at pos 0 -> h
char at pos 1 -> e
char at pos 2 -> l
char at pos 3 -> l
char at pos 4 -> o
char at pos 5 ->
char at pos 6 -> w
char at pos 7 -> o
char at pos 8 -> r
char at pos 9 -> l
char at pos 10 -> d
Sometimes, a while loop is more appropriate. Usually, when we want to modify the index or the string during the loop.
sentence = "This is a very long sentence where you need to find something."
def search(sentence, word):
i = 0
w_len = len(word)
n = len(sentence) - w_len
while i < n and sentence[i:i + w_len] != word:
i += 1
if i < n:
print(f"Found '{word}' at position {i}")
else:
print(f"Could not find '{word}'")
search(sentence, "need")
search(sentence, "sentence")
search(sentence, "long sen")
search(sentence, "loop")Found 'need' at position 39
Found 'sentence' at position 20
Found 'long sen' at position 15
Could not find 'loop'
For whole word search, this would also work (and actually be more efficient):
def search_word(sentence, word):
for sword in sentence.split():
if sword == word:
print(f"Found '{word}'")
break
else: # this gets executed if we don't 'break out' of the loop
print(f"Could not find '{word}'")
search_word(sentence, "need")
search_word(sentence, "loop")
search_word(sentence, "long sen") # not a whole wordFound 'need'
Could not find 'loop'
Could not find 'long sen'
It is also possible to loop through a string using comprehension syntax. This can make the code more readable but it is appropriate only for simple tasks.
The ‘standard’ method of comparing strings using ==
provides a hard comparison: the strings are either the
same or not. Sometimes, however, we might need a soft
comparison, i.e., we might want to measure similarity
of the strings. There is a group of string (or any sequence, actually)
similarity measures, called edit
distances. Examples of edit distances is the Levenshtein
distance, Hamming
distance or the Longest
Common Subsequence. They are called edit distances, since they
measure how many changes, i.e., ‘edits’ need to be done to one string in
order to transform it to the other string. These measures differ in what
types of edits are allowed: - substitutions (change a character into
another) - deletions - insertions - transpositions (“moving” characters
around; e.g., “abc” vs “bca” - ‘a’ moved to the end)
Here is an example of the Hamming distance:
def hamming_distance(s1, s2):
"""
Calculate the Hamming distance between two equal-length strings.
Returns the number of positions where the characters differ.
"""
if len(s1) != len(s2):
raise ValueError("Strings must be of equal length")
return sum(c1 != c2 for c1, c2 in zip(s1, s2))
print(hamming_distance("karolina", "kathrina"))3
Hamming distance allows only substitutions, therefore, the strings must be of equal length.
The zip(s1, s2) expects s1, s2 to be “iterables” (i.e.,
supports iteration - sequential listing of items), just like a for or
while loop. Iterable must implement __iter__ method, which
returns an iterator object. Iterator object must implement
__next__ method, which returns the next item in the
sequence. Lists, tuples, and strings are iterable (custom objects
supporting the protocol described above can be also iterables).
The zip(s1, s2) returns an iterator of tuples, where the
first item is an item in sequence from s1, the second item
is an item in sequence from s2 (more inputs can be
provided, as long as all of them are iterables):
[('k', 'k'), ('a', 'a'), ('r', 't'), ('o', 'h'), ('l', 'r'), ('i', 'i'), ('n', 'n'), ('a', 'a')]
This code makes a generator expression, which can be used as iterables.
The sum function returns the sum of all elements in the
iterator. Technically, the generator returns a list of True
or False values but they are converted to integers (1 or 0)
and then summed.
There is also a build-in library in Python computing similarity of
strings (texts), called difflib.
from difflib import SequenceMatcher
def similarity(s1, s2):
matcher = SequenceMatcher(None, s1, s2)
return matcher.ratio()
def longest_common_subsequence(s1, s2):
matcher = SequenceMatcher(None, s1, s2)
lcs = matcher.find_longest_match(0, len(s1), 0, len(s2))
return s1[lcs.a : lcs.a + lcs.size]
s1 = "This is a very long sentence where you need to find something."
s2 = "This is a very long sentence."
s3 = "This is not a can of words where you need to fly something, or whatever."
print(f'{"s1 self-similarity":<20}: {similarity(s1, s1)}')
print(f'{"s1 self-lcs":<20}: "{longest_common_subsequence(s1, s1)}"')
print(f'{"s1 to s2 similarity":<20}: {similarity(s1, s2)}')
print(f'{"s1 to s2 lcs":<20}: "{longest_common_subsequence(s1, s2)}"')
print(f'{"s1 to s3 similarity":<20}: {similarity(s1, s3)}')
print(f'{"s1 to s3 lcs":<20}: "{longest_common_subsequence(s1, s3)}"')s1 self-similarity : 1.0
s1 self-lcs : "This is a very long sentence where you need to find something."
s1 to s2 similarity : 0.6373626373626373
s1 to s2 lcs : "This is a very long sentence"
s1 to s3 similarity : 0.6417910447761194
s1 to s3 lcs : " where you need to f"
First step in reading files is to specify the file path. Python
allows two ways to manipulate file paths: - processing paths as strings,
using (mostly) the os.path module - processing paths using
the pathlib module
While the pathlib module is more powerful, it is also
more complex to use. Here, we will stick to the os.path
module. Although, in practice, pathlib is recommended, as
it is more robust (e.g., cross-platform).
Absolute paths are paths referenced to the “root” of the file system. Relative paths are referenced to the current working directory.
file_path = "data/rur.txt"
# check if the path is absolute
print(os.path.isabs(file_path)) # False
# convert relative path to absolute
print(os.path.abspath(file_path))
# convert absolute path to relative
print(os.path.relpath(os.path.abspath(file_path)))
# get the absolute path of a file in the parent directory
print(os.path.abspath(os.path.join("..", "rur.txt")))The os.path.exists function checks if a path exists. You
can also check if a path is a file or a directory.
When opening files, it is important to choose the correct mode: - “r”: Read (default, might be omitted) - “w”: Write (overwrites/deletes existing files) - “x”: Write but fail if the file already exists - “a”: Append (adds to the end of the file) - “r+”: Read and write (update the file)
The best/safest way to open a file (whether for reading or writing)
is to use the with statement. This ensures proper closure
of the file.
The syntax is with open(path, mode) as my_file:, where
path is the file path and mode is the file
mode. The my_file variable holds the reference to the
opened file
buffer.
The open(...) method also takes encoding
argument (useful only for text files, not binary). For best
compatibility, it is recommended to use `encoding=“utf-8” (usually the
default).
For example:
# Open a file for reading (default mode)
with open("example.txt", "r") as f:
content = f.read()
# Open a file for writing (this will overwrite the file)
with open("example.txt", "w") as f:
f.write("This is a new file content.\n")
# Open a file for appending
with open("example.txt", "a") as f:
f.write("Appending this line.\n")In practice, it is a good idea to check if the file exists first. The write mode creates a new file, if it does not exist (but overwrites if it does) but the read mode will fail if the file does not exist.
Writing data to files can be done using the write() or
writelines() methods.
write writes a single string to a file:
writelines writes a list of strings to a file:
You can read an entire file at once with read() or read
it line-by-line with readline(). Using read is
fine from smaller files. However, for larger files,
readline is a better option as it does not load the entire
file into memory. Of course, this also means it will need to constantly
access the file on the disk (which is slower than accessing string data
from memory).
Line 1
Line 2
Line 3
Line 1: Line 1
Line 2: Line 2
Line 3: Line 3
Appending is useful when you want to add new data without overwriting the existing content. This is useful for e.g., log files.
Line 1
Line 2
Line 3
An appended line #1.
Line 1
Line 2
Line 3
An appended line #1.
An appended line #2.
Line 1
Line 2
Line 3
An appended line #1.
An appended line #2.
An appended line #3.
Small files: f.read() is simplest /
sufficient. Large files: iterate, i.e.:
This is memory-friendly and leverages buffering.
For large files, writing in larger chunks is better - limits memory usage and the number of system calls.
Python has built-in support for JSON and popular libraries (like PyYAML) for YAML. JSON and YAML are widely used for configuration files, data exchange, and more due to their human-readable formats. They basically store dictionary data, which can be nested and contain other data types, like lists.
import json
import yaml # Requires installation: pip install PyYAML
# JSON example
data_json = {"name": "Alice", "age": 28, "occupation": "Engineer"}
with open("data.json", "w") as f:
json.dump(data_json, f, indent=2)
with open("data.json", "r") as f:
loaded_json = json.load(f)
print("Loaded JSON:", loaded_json)
# YAML example
data_yaml = {"name": "Bob", "age": 34, "occupation": "Designer"}
with open("data.yaml", "w") as f:
yaml.dump(data_yaml, f)
with open("data.yaml", "r") as f:
loaded_yaml = yaml.safe_load(f)
print("Loaded YAML:", loaded_yaml)Loaded JSON: {'name': 'Alice', 'age': 28, 'occupation': 'Engineer'}
Loaded YAML: {'age': 34, 'name': 'Bob', 'occupation': 'Designer'}
Binary files differ from text files as they store data in bytes. Binary files are typically not human readable but allow you to create non-text data, such as images, audio, and video.
img = [
[100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
[100, 0 , 0 , 0, 0 , 0 , 0, 0, 0, 100],
[ 0, 0, 128, 128, 0, 0, 128, 128, 0, 0],
[ 0, 0, 255, 255, 0, 0, 255, 255, 0, 0],
[ 0, 0, 255, 255, 0, 0, 255, 255, 0, 0],
[ 0, 0, 128, 128, 0, 0, 128, 128, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 200, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 200, 0, 0, 0, 0],
[ 0, 0, 0, 0, 200, 200, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 255, 0, 0, 0, 0, 0, 0, 255, 0],
[ 0, 255, 255, 255, 255, 255, 255, 255, 255, 0],
[ 0, 0, 255, 128, 128, 128, 128, 255, 0, 0],
[ 0, 0, 0, 255, 255, 255, 255, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 100, 100, 0, 0, 0, 0],
]
# Create a simple PPM image (binary format)
width, height = len(img[0]), len(img)
# w, h = 100, 100
with open("image.ppm", "wb") as f:
f.write(f"P6 {width} {height} 255\n".encode()) # Write PPM header
for y in range(height):
row = img[y]
for x in range(width):
# Create a gradient: pixel intensity varies with x-coordinate
val = row[x]
# For grayscale, output the same value for R, G, and B
f.write(bytes([val, val, val]))CompletedProcess(args=['convert', 'image.ppm', 'image.png'], returncode=0)
The created image:

Parsing strings means separating strings into some meaningful
“tokens” (bits of string with some predefined meaning). There are
multiple ways how to do it. We will look at parsing using stacks &
queues later, when we discuss stack and queue ADTs. Here, we will show
how to parse strings with the split method.
Let’s first load some data:
Here is some table:
| id | name | volume | radius | max_prop | contained_object |
| --- | --- | --- | --- | --- | --- |
| 0 | Teapot | 1.4 | 0.12 | Awesomeness | Apple, Banana |
| 1 | Blender | 0.8 | 0.25, 0.15 | Coolness Factor | Carrot |
| 2 | Mug, Cup | 1.9 | 0.08 | Radness Level | Potato |
| 3 | Saucepan | 0.5 | 0.22 | Funkiness Quotient | Toast, Bread |
| 4 | Pitcher | 1.1 | 0.18 | Grooviness Index | Coffee Bean |
Now, we want to parse the data: 1) Firstly, extract field names from the table header
Field names: ['id', 'name', 'volume', 'radius', 'max_prop', 'contained_object']
records = []
for line in table_lines[2:]: # skip the header and the separator
split_clean_line = line.strip("| ").split("|")
if len(split_clean_line) < len(field_names):
continue
print(f"Line: {split_clean_line}", line)
record = {}
for i, value in enumerate(split_clean_line):
values = value.strip().split(",")
if len(values) > 1:
record[field_names[i]] = [v.strip() for v in values]
else:
record[field_names[i]] = values[0]
records.append(record)Line: ['0 ', ' Teapot ', ' 1.4 ', ' 0.12 ', ' Awesomeness ', ' Apple, Banana'] | 0 | Teapot | 1.4 | 0.12 | Awesomeness | Apple, Banana |
Line: ['1 ', ' Blender ', ' 0.8 ', ' 0.25, 0.15 ', ' Coolness Factor ', ' Carrot'] | 1 | Blender | 0.8 | 0.25, 0.15 | Coolness Factor | Carrot |
Line: ['2 ', ' Mug, Cup ', ' 1.9 ', ' 0.08 ', ' Radness Level ', ' Potato'] | 2 | Mug, Cup | 1.9 | 0.08 | Radness Level | Potato |
Line: ['3 ', ' Saucepan ', ' 0.5 ', ' 0.22 ', ' Funkiness Quotient ', ' Toast, Bread'] | 3 | Saucepan | 0.5 | 0.22 | Funkiness Quotient | Toast, Bread |
Line: ['4 ', ' Pitcher ', ' 1.1 ', ' 0.18 ', ' Grooviness Index ', ' Coffee Bean'] | 4 | Pitcher | 1.1 | 0.18 | Grooviness Index | Coffee Bean |
Record 0:
id : 0
name : Teapot
volume : 1.4
radius : 0.12
max_prop : Awesomeness
contained_object : ['Apple', 'Banana']
Record 1:
id : 1
name : Blender
volume : 0.8
radius : ['0.25', '0.15']
max_prop : Coolness Factor
contained_object : Carrot
Record 2:
id : 2
name : ['Mug', 'Cup']
volume : 1.9
radius : 0.08
max_prop : Radness Level
contained_object : Potato
Record 3:
id : 3
name : Saucepan
volume : 0.5
radius : 0.22
max_prop : Funkiness Quotient
contained_object : ['Toast', 'Bread']
Record 4:
id : 4
name : Pitcher
volume : 1.1
radius : 0.18
max_prop : Grooviness Index
contained_object : Coffee Bean
Be careful when splitting text by a character. Sometimes, the same character might be a part of text, e.g.:
Wrong splitting: ['up', ' down', ' apple', 'banana', ' cucumber']
Right splitting: ['up', 'down', 'apple,banana', 'cucumber']
This was a simple example but sometimes, thing might get more tricky. E.g., if comma is used between numbers = don’t split, otherwise split. That is, we want to split also when comma is used without space between letters. In such cases, we can either loop through the text and replace commas between numbers with another character (we can then replace it back). Or, we can use what’s called regular expressions. We will, however, not go into that topic here.
we want to print the parsed data back into a ‘nice’ table:
column_width = 18
header = '|' + '|'.join([f"{name:^{column_width}}" for name in field_names]) + '|'
print(header)
separator = '|' + '|'.join(['-' * column_width for name in field_names]) + '|'
print(separator)
rows = []
for ri, record in enumerate(records):
row = []
for value in record.values():
if isinstance(value, list):
row.append(f"{', '.join(value):^{column_width}}")
else:
row.append(f"{value:^{column_width}}")
rows.append('|' + '|'.join(row) + '|')
print('\n'.join(rows))| id | name | volume | radius | max_prop | contained_object |
|------------------|------------------|------------------|------------------|------------------|------------------|
| 0 | Teapot | 1.4 | 0.12 | Awesomeness | Apple, Banana |
| 1 | Blender | 0.8 | 0.25, 0.15 | Coolness Factor | Carrot |
| 2 | Mug, Cup | 1.9 | 0.08 | Radness Level | Potato |
| 3 | Saucepan | 0.5 | 0.22 |Funkiness Quotient| Toast, Bread |
| 4 | Pitcher | 1.1 | 0.18 | Grooviness Index | Coffee Bean |