Python Foundations Comprehensive Study Notes
Python Foundations Comprehensive Study Notes
Technical Notes
1. Python Introduction & Basics
1.1. Python Introduction
Python, a high-level, interpreted programming language, was conceived by Guido van Rossum
in the late 1980s. Its design philosophy emphasizes code readability and simplicity, allowing
developers to express complex concepts in fewer lines of code compared to languages like C++
or Java. The name "Python" was inspired by the British comedy group Monty Python's Flying
Circus, reflecting a touch of humor in its origins.
The evolution of Python can be marked by several key releases:
● Python 1.0 (1994): This version introduced core features like dynamic typing and built-in
data structures, setting the stage for Python's versatility and ease of use.
● Python 2.x Era: This period saw significant growth in Python's popularity and an
expansion of its capabilities. Features such as list comprehensions and improved Unicode
support were introduced, broadening its applicability in areas like web development and
scientific computing.
● Python 3.x Era: Representing a major revision, Python 3 aimed to modernize the
language by introducing enhancements, improving performance, and ensuring more
consistent syntax. A key aspect of this release was its break in backward compatibility
with Python 2, a deliberate decision to rectify fundamental design flaws and allow the
language to evolve unencumbered by legacy constraints. This transition, though initially
challenging for the community, was pivotal for Python's long-term viability and relevance
in a rapidly changing technological landscape.
Python's design incorporates several key features that contribute to its widespread adoption:
● Simplicity and Readability: Python's syntax is designed to be clean and closely
resemble the English language, making it relatively easy to learn and understand. This
allows developers to focus on problem-solving rather than grappling with complex
language rules.
● Interpreted Language: Python code is executed line by line by an interpreter, rather than
being compiled into machine code all at once. This facilitates easier debugging and offers
greater flexibility during development. The analogy of a live interpreter translating speech
in real-time effectively illustrates this process.
● Platform Independent: The interpreted nature of Python contributes to its platform
independence. Python programs can typically run on various operating systems (e.g.,
Windows, macOS, Linux) without requiring code modifications.
● Dynamically Typed: As will be discussed further, variable types are checked during
runtime, meaning explicit type declarations are not required.
● Extensive Standard Library and Third-Party Packages: Python boasts a vast
collection of modules and packages that provide pre-written code for a wide array of
tasks, significantly reducing development time. This is often likened to having a
well-equipped workshop with tools for almost any job.
● Object-Oriented, Imperative, and Functional Programming Paradigms: Python
supports multiple programming paradigms, allowing developers to choose the style that
best suits their problem. Its data model is inherently object-oriented.
These features have led to Python's application in diverse fields, including:
● Web Development (e.g., Django, Flask)
● Data Science and Machine Learning (e.g., NumPy, Pandas, Scikit-learn, TensorFlow)
● Scientific and Numeric Computing
● Automation and Scripting
● Software Development and GUI Applications
The official Python Language Reference describes the syntax and core semantics of the
language, covering lexical analysis (how code is tokenized), the data model (objects, values,
types), and the execution model (program structure, naming, exceptions).
Setting up a Python development environment typically involves installing a Python interpreter
and, crucially, using virtual environments to manage project-specific dependencies. Virtual
environments, created using tools like venv (standard library) or virtualenv (third-party), isolate
project dependencies, preventing conflicts between different projects that might require different
versions of the same package.
Steps for Environment Setup (General Overview):
1. Install Python: Download the appropriate Python installer for the operating system from
the official Python website or use package managers like Homebrew (macOS) or apt
(Debian/Ubuntu).
2. Create a Virtual Environment: Navigate to the project directory and use the command
python -m venv <environment_name> (e.g., python -m venv env).
3. Activate the Virtual Environment:
○ On macOS/Linux: source <environment_name>/bin/activate.
○ On Windows: .\<environment_name>\Scripts\activate.
4. Install Packages: Once activated, use pip install <package_name> to install necessary
libraries within the isolated environment.
5. Deactivate: Use the command deactivate to exit the virtual environment.
Variable Naming Rules (PEP 8 Conventions): Valid variable names must adhere to specific
rules :
● Must begin with a letter (a-z, A-Z) or an underscore (_).
● Can contain letters, numbers (0-9), and underscores after the first character.
● Are case-sensitive (myVar and myvar are distinct variables).
● Cannot be a Python reserved keyword (e.g., if, for, while, def). PEP 8, the style guide for
Python code, recommends using snake_case (lowercase with words separated by
underscores) for variable names (e.g., user_name, total_amount).
Variable Scope (LEGB Rule): The scope of a variable determines its accessibility within the
program. Python follows the LEGB rule for name resolution :
● L (Local): Names assigned within a function (def or lambda). These are not visible
outside the function. Example:
def my_func():
message = "Hello from local scope" # Local variable
print(message)
my_func()
# print(message) # This would cause a NameError
● E (Enclosing function locals): Names in the local scope of any enclosing functions
(e.g., in nested functions). The nonlocal keyword can be used to modify such variables.
Example:
def outer_function():
text = "Outer variable" # Enclosing scope variable
def inner_function():
nonlocal text # Declare intent to modify 'text' from
enclosing scope
text = "Inner variable modified outer"
print(text)
inner_function()
print(text) # Shows the modification
outer_function()
● G (Global): Names assigned at the top level of a module file, or declared global in a
function using the global keyword. Example:
count = 100 # Global variable
def show_count():
print(count) # Accesses global 'count'
def update_count():
global count # Declare intent to modify global 'count'
count += 1
show_count() # Output: 100
update_count()
show_count() # Output: 101
● B (Built-in): Pre-assigned names in Python (e.g., len(), print(), True, None). These are
always available.
Python searches for a name in this order: Local -> Enclosing -> Global -> Built-in. The first
occurrence found is used.
Constants: While Python doesn't have true constants (whose values cannot be reassigned),
the convention is to use all uppercase letters for names intended to be constants (e.g., PI =
3.14159, MAX_CONNECTIONS = 100). This signals to other developers that the value should
not be changed.
Built-in Data Types: Python offers several fundamental built-in data types :
● Numeric Types:
○ int (Integer): Whole numbers (positive, negative, or zero) of unlimited size.
Example: age = 25.
○ float (Floating-Point Number): Numbers with a decimal point. Example: price =
19.99.
○ complex (Complex Number): Numbers with a real and imaginary part, written as a
+ bj. Example: z = 2 + 3j.
● Sequence Types:
○ str (String): Ordered, immutable sequence of Unicode characters. Enclosed in
single ('...'), double ("..."), or triple ('''...''' or """...""") quotes. Example: name =
"Python".
○ list: Ordered, mutable sequence of items. Enclosed in square brackets ``. Example:
numbers = .
○ tuple: Ordered, immutable sequence of items. Enclosed in parentheses ().
Example: coordinates = (10, 20).
○ range: Immutable sequence of numbers, commonly used for looping. Example:
range(0, 5).
● Mapping Type:
○ dict (Dictionary): Unordered (ordered in Python 3.7+) collection of key-value pairs.
Enclosed in curly braces {}. Keys must be immutable and unique. Example: person
= {"name": "Alice", "age": 30}.
● Set Types:
○ set: Unordered collection of unique, immutable items. Mutable. Enclosed in curly
braces {}. Example: unique_numbers = {1, 2, 3}.
○ frozenset: Unordered collection of unique, immutable items. Immutable. Created
using frozenset(). Example: frozen_colors = frozenset({"red", "green"}).
● Boolean Type:
○ bool: Represents truth values, True or False. Example: is_active = True.
● None Type:
○ NoneType: Has a single value, None, used to represent the absence of a value or
a null value. Example: result = None.
● Binary Types: bytes, bytearray, memoryview for handling binary data.
Type Checking and Conversion:
● type(): Returns the type of an object. Example: type(10) returns <class 'int'>.
● isinstance(): Checks if an object is an instance of a particular class or type. Example:
isinstance(10, int) returns True.
● Type Conversion (Casting): Python allows explicit conversion between compatible data
types using built-in functions like int(), float(), str(), list(), tuple(), set(), dict(), bool().
○ Example: str_num = "123"; num = int(str_num) converts the string "123" to the
integer 123.
○ Example: float_val = 3.9; int_val = int(float_val) results in int_val being 3
(truncation).
○ Python also performs implicit type conversion (coercion) in certain situations, like
adding an integer and a float, where the result is a float to avoid data loss. For
instance, 1 + 2.0 results in 3.0.
Mutable and Immutable Types:
● Immutable: Objects whose state cannot be changed after creation. Examples: int, float,
str, tuple, frozenset. Operations that appear to modify them actually create new objects.
x = 10
print(id(x)) # Memory address of x
x = x + 1 # A new integer object 11 is created, and x now
refers to it
print(id(x)) # Different memory address
my_tuple = (1, 2, 3)
# my_tuple = 4 # This would raise a TypeError
● Mutable: Objects whose state can be changed after creation. Examples: list, dict, set.
my_list =
print(id(my_list)) # Memory address of my_list
my_list.append(4) # Modifies the original list object in place
print(id(my_list)) # Same memory address
print(my_list) # Output:
The distinction between mutable and immutable types is fundamental in Python. When an
immutable object is "changed," a new object is created in memory, and the variable name is
rebound to this new object. For mutable objects, operations can modify the object in-place
without creating a new object. This has implications for how variables are shared and modified,
especially when passed to functions or when multiple variables reference the same mutable
object. For example, if list_a = and list_b = list_a, modifying list_a (e.g., list_a.append(3)) will
also change list_b because both names refer to the same list object in memory. This concept of
shared references is crucial.
Assigning Multiple Variables: Python allows assigning a single value to multiple variables or
multiple values to multiple variables in a single line.
● Single value to multiple variables: x = y = z = 10
● Multiple values to multiple variables (packing/unpacking): a, b, c = 1, 2, 3
Deleting Variables: The del keyword can be used to remove a variable from the current
namespace. Example: x = 10; del x; # print(x) would raise NameError
2. Control Flow
Control flow statements dictate the order in which the statements in a program are executed.
Python provides conditional statements for decision-making and loops for repetitive execution of
code blocks.
● if-else Statement: The if-else statement provides an alternative block of code (the else
block) to be executed if the if condition evaluates to False. Syntax:
if expression:
# statement(s) to execute if expression is True
else:
# statement(s) to execute if expression is False
Example:
num = 7
if num % 2 == 0:
print("The number is even.")
else:
print("The number is odd.") # This will be printed
● if-elif-else Ladder: For situations involving multiple mutually exclusive conditions, the
if-elif-else ladder (where elif stands for "else if") is used. Python evaluates the conditions
sequentially. The code block associated with the first condition that evaluates to True is
executed, and the rest of the ladder is skipped. An optional else block at the end can
serve as a default case if none of the preceding if or elif conditions are met. Syntax:
if condition1:
# code block 1
elif condition2:
# code block 2
elif condition3:
# code block 3
#... more elif blocks
else:
# default code block (optional)
Example:
score = 85
if score >= 90:
print("Grade: A")
elif score >= 80:
print("Grade: B") # This will be printed
elif score >= 70:
print("Grade: C")
else:
print("Grade: F")
The order of elif conditions is crucial, particularly if the conditions are not strictly mutually
exclusive. For instance, in the score example, if the condition score >= 80 were checked
before score >= 90, a score of 95 would incorrectly result in "Grade: B". Therefore, more
specific conditions should generally precede more general ones if overlap is possible. The
else block plays a vital role in handling cases not explicitly covered by the if or elif
statements, acting as a catch-all and contributing to robust program design by preventing
unhandled scenarios. Python's truth value testing, where non-empty sequences and
non-zero numbers evaluate to True, can lead to concise conditions like if my_list:, which
checks if my_list is not empty. While Pythonic, the explicitness of direct boolean
comparisons might be preferred for clarity in some contexts.
● Nested if Statements: Conditional statements can be nested within one another to
create more complex decision-making logic. An if, if-else, or if-elif-else statement can be
part of the code block of another conditional statement. Syntax:
if condition1:
# statement(s) for condition1
if condition2:
# statement(s) for condition1 AND condition2
else:
# statement(s) for condition1 AND NOT condition2
else:
# statement(s) if condition1 is False
Example:
num = 10
if num > 0:
print("Number is positive.")
if num % 2 == 0:
print("Number is also even.") # This will be printed
else:
print("Number is also odd.")
else:
print("Number is not positive.")
○ Lists:
colors = ["red", "green", "blue"]
for color in colors:
print(color)
# Output: red, green, blue (each on a new line)
○ Tuples:
coordinates = (10, 20, 30)
for coord in coordinates:
print(coord)
# Output: 10, 20, 30 (each on a new line)
○ range(start, stop): Generates numbers from start up to (but not including) stop.
for i in range(1, 4):
print(i) # Output: 1, 2, 3
Nested for Loops: A for loop can be placed inside another for loop. The inner loop
completes all its iterations for each single iteration of the outer loop.for i in
range(2): # Outer loop iterates twice (i=0, i=1)
for j in range(3): # Inner loop iterates thrice for each i
(j=0, j=1, j=2)
print(f"i={i}, j={j}")
# Output:
# i=0, j=0
# i=0, j=1
# i=0, j=2
# i=1, j=0
# i=1, j=1
# i=1, j=2
● while Loops: while loops execute a block of code repeatedly as long as a given Boolean
condition remains True. The condition is checked before each iteration. Syntax:
while condition:
# statement(s) to execute as long as condition is True
Counter-Controlled Example:
count = 1
while count <= 3:
print(f"Count is: {count}")
count += 1 # Important: update the condition variable
# Output: Count is: 1, Count is: 2, Count is: 3
If the variable controlling the condition is not updated correctly within the loop, it can lead
to an infinite loop.Sentinel-Controlled Example (Conceptual): This type of loop
continues until a special value (sentinel) is encountered.
# Conceptual example, requires user input
# entry = ""
# while entry.lower()!= "exit":
# entry = input("Enter command (or 'exit' to quit): ")
# if entry.lower()!= "exit":
# print(f"Processing: {entry}")
Nested while Loops: Similar to for loops, while loops can be nested.
outer_count = 1
while outer_count <= 2:
inner_count = 1
while inner_count <= 2:
print(f"Outer: {outer_count}, Inner: {inner_count}")
inner_count += 1
outer_count += 1
# Output:
# Outer: 1, Inner: 1
# Outer: 1, Inner: 2
# Outer: 2, Inner: 1
# Outer: 2, Inner: 2
Infinite Loops: A loop whose condition always evaluates to True (e.g., while True:) will
run indefinitely unless explicitly terminated by a break statement or an external
interruption.
● continue Statement: The continue statement skips the rest of the code inside the current
iteration of the innermost for or while loop and proceeds directly to the next iteration. For
for loops, it moves to the next item in the iterable. For while loops, it re-evaluates the
loop's condition. Syntax: continue Example:
for i in range(1, 6): # Numbers from 1 to 5
if i == 3:
continue # Skip printing when i is 3
print(i)
# Output: 1, 2, 4, 5
print("Loop finished.")
● pass Statement: The pass statement is a null operation; nothing happens when it is
executed. It is used as a placeholder where a statement is syntactically required, but no
code needs to be executed. This is common in defining empty functions, classes, or
conditional blocks that will be implemented later. Syntax: pass Example:
def my_empty_function():
pass # Placeholder for future implementation
for i in range(3):
if i == 1:
pass # Do nothing specific for i=1
else:
print(i)
# Output: 0, 2
● else Clauses on Loops: A unique feature in Python is the ability to have an else clause
associated with for and while loops. The else block is executed only if the loop terminates
normally—that is, it completes all its iterations (for a for loop) or its condition becomes
False (for a while loop), without being exited by a break statement.This behavior is distinct
from the else in an if statement. It is particularly useful in search scenarios: if the loop
finds the item and breaks, the else is skipped; if the loop completes without finding the
item (no break), the else block can execute code indicating the item was not found. This
often leads to more readable code than using a separate flag variable. The else clause on
a loop shares more in common with the else clause of a try...except block, which runs if
no exception occurs.Syntax (for for loop):
for item in iterable:
if condition_met_for_break:
# process item
break
# process item
else:
# executed if the loop completed without a break
Example (for...else): Searching for a prime number
num_to_check = 7
for i in range(2, num_to_check):
if num_to_check % i == 0:
print(f"{num_to_check} is not a prime number, it is
divisible by {i}.")
break
else: # Belongs to the for loop
print(f"{num_to_check} is a prime number.")
# Output: 7 is a prime number.
If num_to_check was, for example, 6, the inner if would be true for i=2, "6 is not a prime..."
would print, break would execute, and the else block would be skipped.Syntax (for while
loop):
while condition:
if condition_met_for_break:
# process
break
# process
else:
# executed if the loop condition became False (and no break
occurred)
Example (while...else):
attempts = 3
while attempts > 0:
password = input("Enter password: ")
if password == "secret":
print("Access granted.")
break
attempts -= 1
print(f"Incorrect. {attempts} attempts remaining.")
else: # Belongs to the while loop
print("Access denied. No attempts left.")
If the correct password is entered, "Access granted" prints, break executes, and the else
block is skipped. If all attempts fail, the loop condition attempts > 0 eventually becomes
false, the loop terminates normally, and "Access denied..." is printed.
3. String Manipulation
Strings are fundamental data types in Python, representing immutable sequences of Unicode
characters. Python provides a rich set of tools for creating, accessing, and manipulating strings.
● Slicing: A portion of a string (a substring) can be extracted using slicing. The syntax is
string[start:end:step].
○ start: The starting index (inclusive). Defaults to 0 if omitted.
○ end: The ending index (exclusive). Defaults to the length of the string if omitted.
○ step: The amount to increment the index by. Defaults to 1 if omitted.
s = "Programming"
print(s[0:7]) # Output: Program (characters from index 0 to 6)
print(s[:7]) # Output: Program (from the beginning up to
index 6)
print(s[8:]) # Output: ming (from index 8 to the end)
print(s[::2]) # Output: Pormig (every second character)
print(s[::-1]) # Output: gnimmargorP (reverses the string)
● Immutability: Strings are immutable. This means that operations like concatenation or
character replacement do not modify the original string; instead, they create and return a
new string.
s1 = "hello"
s2 = s1 + " world" # s2 is "hello world", s1 is still "hello"
# s1 = "H" # This would raise a TypeError
s1 = "H" + s1[1:] # Creates a new string "Hello" and reassigns s1
print(s1) # Output: Hello
This immutability has important implications. It makes strings predictable and safe to use
as dictionary keys because their hash value will never change. However, for operations
involving many modifications (like building a long string by repeatedly appending
characters in a loop), creating numerous intermediate string objects can be inefficient. In
such cases, techniques like joining a list of strings ("".join(list_of_strings)) are preferred
because they are optimized to pre-calculate the required memory and build the final string
in a more efficient manner, often in a single pass after collecting all parts.
● Concatenation (+) and Repetition (*):
○ The + operator concatenates (joins) strings: greeting = "Hello" + " " + "World"
results in "Hello World".
○ The * operator repeats a string a specified number of times: laugh = "ha" * 3 results
in "hahaha".
● Raw Strings: A raw string literal is prefixed with r or R. In a raw string, backslashes (\) are
treated as literal characters rather than initiating escape sequences. This is particularly
useful for regular expression patterns and Windows file paths, where backslashes have
special meanings. Syntax: raw_path = r"C:\Users\new_folder\notes.txt" Without the r
prefix, \n would be interpreted as a newline character. Note: A raw string cannot end with
an odd number of backslashes, as the final backslash would attempt to escape the
closing quote.
● Unicode Character Representation: Python strings are inherently sequences of
Unicode code points, allowing them to represent characters from virtually all writing
systems.
○ Direct Inclusion: Unicode characters can often be typed directly into string literals
if the source file encoding (typically UTF-8) supports them: text = "你好, мир, γειά".
○ Escape Sequences: Unicode characters can be represented using escape
sequences:
■ \uXXXX: For 16-bit Unicode code points (four hexadecimal digits). Example:
'\u20AC' represents the Euro sign (€).
😀
■ \UXXXXXXXX: For 32-bit Unicode code points (eight hexadecimal digits).
Example: '\U0001F600' represents a grinning face emoji ( ).
○ ord(char): This built-in function returns the Unicode code point (an integer) of a
given character. Example: ord('A') returns 65; ord('€') returns 8364.
○ chr(code_point): This built-in function returns the character (a string of length 1)
corresponding to a given Unicode code point. Example: chr(65) returns 'A';
chr(8364) returns '€'.
● str.format() Method: This method was introduced in Python 2.6 and offers a powerful
and flexible way to format strings. It uses curly braces {} as placeholders in the string,
which are then filled by arguments passed to the format() method.
○ Syntax: template_string.format(arg1, arg2,..., kwarg1=value1,...)
○ Positional Arguments: Placeholders can be empty {}, or numbered {0}, {1} to refer
to arguments by their position.
print("Hello, {}! You are {} years old.".format("Bob", 25))
# Output: Hello, Bob! You are 25 years old.
print("The {1} {0} jumps over the {2} {0}.".format("dog",
"quick brown fox", "lazy"))
# Output: The quick brown fox dog jumps over the lazy dog.
● %-Formatting (C-style): This is the original string formatting method in Python, similar to
C's printf. It uses the % operator to substitute values into a string.
○ Syntax: format_string % values
○ Specifiers: Common specifiers include %s (string), %d (integer), %f (float).
Precision for floats can be specified, e.g., %.2f.
○ Disadvantages: Can be less readable for multiple substitutions, prone to errors if
types or number of arguments don't match the specifiers.
○ Example:
name = "Charlie"
score = 95.5
print("Player: %s, Score: %.1f" % (name, score))
# Output: Player: Charlie, Score: 95.5
● string.Template Class: This method, part of the string module, is considered safer when
dealing with format strings supplied by users, as it is less prone to unintended execution
of arbitrary code. It uses $-based substitutions.
○ Syntax:
from string import Template
t = Template("Hello, $name! Your ID is $user_id.")
print(t.substitute(name="Dave", user_id="dave123"))
# Output: Hello, Dave! Your ID is dave123.
# Use safe_substitute to avoid KeyError if a placeholder is
missing
print(t.safe_substitute(name="Eve"))
# Output: Hello, Eve! Your ID is $user_id.
○ Using the list() constructor, which can take an iterable (like a tuple, string, or
another list) as an argument:
list_from_tuple = list((1, 2, 3)) # Becomes
list_from_string = list("hello") # Becomes ['h', 'e', 'l',
'l', 'o']
empty_list_constructor = list()
● Indexing and Slicing: Accessing and manipulating list elements is done similarly to
strings:
○ Indexing: Access an individual item using its zero-based index. Negative indexing
is also supported (-1 for the last item).
fruits = ["apple", "banana", "cherry"]
print(fruits) # Output: apple
print(fruits[-1]) # Output: cherry
○ Slicing: Extract a sub-list using list[start:end:step]. The start index is inclusive, end
is exclusive.
numbers =
print(numbers[1:4]) # Output:
print(numbers[:3]) # Output:
print(numbers[::2]) # Output:
○ Updating Elements: Since lists are mutable, individual elements or slices can be
reassigned.
colors = ["red", "green", "blue"]
colors = "yellow" # Changes "green" to "yellow"
print(colors) # Output: ['red', 'yellow', 'blue']
colors[0:2] = ["orange", "purple"] # Replaces first two
elements
print(colors) # Output: ['orange', 'purple', 'blue']
● Common List Methods: Python lists have a variety of built-in methods for
manipulation.Table 4: Common List Methods
Method Syntax Purpose Example
append() list.append(x) Adds element x to the L=; L.append(3) results
end of the list. in L=.
extend() list.extend(iterable) Appends all items from L=; L.extend() results in
iterable to the end of L=.
the list.
insert() list.insert(i, x) Inserts element x at L=; L.insert(1,2) results
index i. in L=.
remove() list.remove(x) Removes the first L=; L.remove(2) results
occurrence of element in L=.
x. Raises ValueError if
x is not found.
pop() list.pop([i]) Removes and returns L=; v=L.pop(1) makes
the item at index i. If i is v=2, L=.
omitted, removes and
returns the last item.
clear() list.clear() Removes all items from L=; L.clear() results in
the list, making it L=.
empty.
index() list.index(x[, start[, Returns the zero-based L=['a','b','c']; L.index('b')
end]]) index of the first is 1.
occurrence of x. Raises
ValueError if x is not
found.
count() list.count(x) Returns the number of L=; L.count(2) is 2.
times x appears in the
list.
sort() list.sort(key=None, Sorts the items of the L=; L.sort() makes L=.
reverse=False) list in-place. key and
reverse allow
customization.
reverse() list.reverse() Reverses the elements L=; L.reverse() makes
of the list in-place. L=.
copy() list.copy() Returns a shallow copy L1=; L2=L1.copy(). L2
of the list. Equivalent to is a new list.
list[:].
The copy() method and slicing ([:]) create a shallow copy. This means a new list object is
created, but the elements within this new list are references to the same objects that the original
list's elements refer to. If a list contains other mutable objects (e.g., nested lists), modifying
these nested objects through one list (original or copy) will affect the other. This is because both
lists hold references to the same nested mutable objects. For example, if a = [, ] and b =
a.copy(), then a and b refer to the same inner list . If `a.append(2)` is executed, this inner list
becomes . Consequently, b will also appear as [, ] because b still points to that same modified
inner list. To create a fully independent copy of a nested structure, a deep copy using the
copy.deepcopy() function from the copy module is necessary. This distinction is critical for
avoiding unintended side effects when working with lists of mutable objects.
○ Comma-separated values (parentheses are optional if the context is clear, but often
recommended for readability):
another_tuple = 10, 20, "see" # Creates a tuple (10, 20,
"see")
○ Empty tuple:
empty_tup = ()
empty_tup_constructor = tuple()
● Indexing and Slicing: Tuples support indexing and slicing in the same way as lists and
strings.
data = ("alpha", "beta", "gamma", "delta")
print(data) # Output: beta
print(data[-2]) # Output: gamma
print(data[0:2]) # Output: ('alpha', 'beta')
● Common Tuple Methods & Functions: Due to their immutability, tuples have fewer
methods than lists.
○ tuple.count(value): Returns the number of times value appears in the tuple.
t = (1, 2, 2, 3, 2)
print(t.count(2)) # Output: 3
○ Built-in Functions: len(), max(), min(), sum() (for numeric tuples), sorted() (returns
a new sorted list), any(), all() work with tuples as they do with other iterables.
● Use Cases (When to prefer tuples over lists): Tuples are often preferred over lists in
specific scenarios due to their immutability and the semantic meaning they can convey :
1. Heterogeneous Fixed Collections: When representing a collection of items that
form a single, fixed record or entity where the order and number of items are
meaningful (e.g., geographic coordinates (latitude, longitude), RGB color values
(red, green, blue), or database records).
2. Data Integrity: Immutability ensures that the data within a tuple cannot be
accidentally modified after creation, which is crucial for maintaining data integrity in
certain parts of a program.
3. Dictionary Keys: Since tuples are immutable (if all their elements are immutable),
they are hashable and can be used as keys in dictionaries. Lists, being mutable,
cannot be used as dictionary keys.
4. Returning Multiple Values from Functions: Python functions can conveniently
return multiple values, which are implicitly packed into a tuple. The caller can then
easily unpack these values.
def get_user_info():
name = "Alice"
age = 30
return name, age # Returns ("Alice", 30)
user_name, user_age = get_user_info()
4.4. Dictionaries
● Definition: Dictionaries in Python are mutable, unordered (ordered in Python 3.7+)
collections that store data as key-value pairs. Each key must be unique and immutable,
while values can be of any data type and can be duplicated. They are highly optimized for
retrieving values when the key is known.
● Characteristics:
○ Key-Value Pairs: Data is stored in key: value mappings.
○ Mutable: Dictionaries can be changed after creation (items can be added, modified,
or removed).
○ Ordered (Python 3.7+): As of Python 3.7, dictionaries remember the insertion
order of items. In earlier versions (Python 3.6 and below), dictionaries were
unordered.
○ Unique Keys: Keys within a dictionary must be unique. If a duplicate key is
assigned, the new value will overwrite the previous value associated with that key.
○ Immutable Keys: Keys must be of an immutable type (e.g., strings, numbers,
tuples containing only immutable elements). Lists or other dictionaries cannot be
used as keys.
○ Heterogeneous: Values can be of any data type. Keys can also be of different
immutable types within the same dictionary.
○ Dynamic: Dictionaries can grow or shrink in size.
○ Hashing: Dictionaries are implemented using hash tables, which allows for efficient
(typically average O(1) time complexity) lookup, insertion, and deletion of items.
● Creation:
○ Using curly braces {} with comma-separated key: value pairs:
person = {"name": "Alice", "age": 30, "city": "New York"}
empty_dict = {} # Creates an empty dictionary
● Deleting Items:
○ del dictionary[key]: Removes the item with the specified key. Raises KeyError if key
is not found.
# Assuming person has "zipcode" key
# del person["zipcode"]
○ dictionary.pop(key, default_value): Removes the item with the specified key and
returns its value. If key is not found, it returns default_value (if provided) or raises
KeyError.
# age = person.pop("age")
# print(f"Removed age: {age}")
● Common Dictionary Methods: Dictionaries provide various methods for accessing keys,
values, items, and performing other operations.Table 5: Common Dictionary Methods
Method Syntax Purpose Example
keys() dictionary.keys() Returns a view object d={'a':1,'b':2};
that displays a list of all k=d.keys(); print(list(k))
the keys in the is ['a','b'].
dictionary.
values() dictionary.values() Returns a view object d={'a':1,'b':2};
that displays a list of all v=d.values();
the values in the print(list(v)) is ``.
dictionary.
items() dictionary.items() Returns a view object d={'a':1,'b':2};
that displays a list of a i=d.items(); print(list(i))
dictionary's key-value is [('a',1),('b',2)].
tuple pairs.
get() dictionary.get(key, Returns the value for d={'a':1}; d.get('a') is 1;
default=None) key if key is in the d.get('c',0) is 0.
dictionary, else default.
If default is not given, it
defaults to None.
pop() dictionary.pop(key, Removes key and d={'a':1,'b':2};
default) returns its value. If key v=d.pop('a'); v is 1, d is
is not found, default is {'b':2}.
returned if given,
otherwise KeyError is
raised.
popitem() dictionary.popitem() Removes and returns a d={'a':1,'b':2};
(key, value) pair as a item=d.popitem(); item
2-tuple. Pairs are could be ('b',2).
returned in LIFO order
(last-in, first-out) for
Python 3.7+.
clear() dictionary.clear() Removes all items from d={'a':1}; d.clear()
the dictionary. makes d={}.
copy() dictionary.copy() Returns a shallow copy d1={'a':1};
of the dictionary. d2=d1.copy(); d2 is a
new dict.
update() dictionary.update([other Updates the dictionary d={'a':1};
]) with the key/value pairs d.update({'b':2,'a':3}); d
from other, overwriting is {'a':3,'b':2}.
existing keys. other can
be another dict or an
iterable of pairs.
setdefault() dictionary.setdefault(ke If key is in the d={'a':1};
y, default=None) dictionary, return its d.setdefault('a',0) is 1;
value. If not, insert key d.setdefault('b',0) is 0, d
with a value of default is {'a':1,'b':0}.
and return default.
default defaults to
Method Syntax Purpose Example
None.
Source: Synthesized from. Examples are illustrative.
The view objects returned by keys(), values(), and items() are dynamic. This means that if the
dictionary changes, the view reflects these changes. This is a powerful feature, as it avoids the
need to create a new list of keys/values/items every time the dictionary is modified if one is
iterating over these views.
The choice of dictionary keys (immutable types) is directly linked to the hashing mechanism that
underpins dictionary performance. Hashing requires that an object's hash value remains
constant throughout its lifetime. Mutable objects, like lists, can change their content, which
would change their hash value, making them unsuitable as dictionary keys. This immutability
requirement for keys ensures the integrity and efficiency of the dictionary's internal hash table.
○ Using the set() constructor, which can take an iterable as an argument. This is also
the only way to create an empty set:
set_from_list = set() # Becomes {1, 2, 3, 4}
empty_set = set() # Creates an empty set
# another_empty_set = {} # This creates an EMPTY
DICTIONARY, not a set!
● Common Set Methods and Operations: Sets support a variety of methods for
modification and mathematical operations.Table 6: Common Set Methods and
Operations
Method/Operation Syntax Purpose Example
add() set.add(element) Adds element to the s={1,2}; s.add(3)
set. If already present, makes s={1,2,3}.
Method/Operation Syntax Purpose Example
no change.
remove() set.remove(element) Removes element from s={1,2,3}; s.remove(2)
the set. Raises makes s={1,3}.
KeyError if element is
not found.
discard() set.discard(element) Removes element from s={1,2,3}; s.discard(4)
the set if present. Does leaves s={1,2,3}.
not raise an error if not
found.
pop() set.pop() Removes and returns s={1,2,3}; x=s.pop(). x
an arbitrary element is one of 1,2,3.
from the set. Raises
KeyError if the set is
empty.
clear() set.clear() Removes all elements s={1,2}; s.clear() makes
from the set, making it s=set().
empty.
union() / ` ` set1.union(set2,...) / Returns a new set
set1 | set2 containing all unique
elements from all sets.
intersection() / & set1.intersection(set2,.. Returns a new set s1={1,2};s2={2,3};
.) / set1 & set2 containing only s1.intersection(s2) is
elements common to all {2}.
sets.
difference() / - set1.difference(set2,...) Returns a new set with s1={1,2};s2={2,3};
/ set1 - set2 elements from set1 that s1.difference(s2) is {1}.
are not in set2 (and
other sets).
symmetric_difference() set1.symmetric_differe Returns a new set with s1={1,2};s2={2,3};
/^ nce(set2) / set1 ^ set2 elements in exactly one s1.symmetric_differenc
of the sets (elements in e(s2) is {1,3}.
either set1 or set2, but
not both).
issubset() / <= set1.issubset(set2) / Returns True if set1 is s1={1,2};s2={1,2,3};
set1 <= set2 a subset of set2. s1.issubset(s2) is True.
issuperset() / >= set1.issuperset(set2) / Returns True if set1 is s1={1,2,3};s2={1,2};
set1 >= set2 a superset of set2. s1.issuperset(s2) is
True.
isdisjoint() set1.isdisjoint(set2) Returns True if set1 s1={1,2};s2={3,4};
and set2 have no s1.isdisjoint(s2) is True.
elements in common.
update() / ` =` set1.update(set2,...) / Adds elements from
set1 |= set2 set2 (and others) to
set1. (In-place union)
intersection_update() / set1.intersection_updat Updates set1 keeping s1={1,2,3};s2={2,4};
&= e(set2,...) / set1 &= only elements found in s1.intersection_update(
Method/Operation Syntax Purpose Example
set2 both set1 and set2. s2) makes s1={2}.
(In-place intersection)
difference_update() / -= set1.difference_update( Updates set1 removing s1={1,2,3};s2={2,4};
set2,...) / set1 -= set2 elements found in set2. s1.difference_update(s
(In-place difference) 2) makes s1={1,3}.
symmetric_difference_ set1.symmetric_differe Updates set1 with s1={1,2,3};s2={2,4};
update() / ^= nce_update(set2) / set1 elements in either set1 s1.symmetric_differenc
^= set2 or set2 but not both. e_update(s2) makes
(In-place symmetric s1={1,3,4}.
difference)
copy() set.copy() Returns a shallow copy s1={1,2}; s2=s1.copy().
of the set. s2 is a new set.
Source: Synthesized from. Examples are illustrative. The non-operator versions of methods like
union(), intersection(), etc., can accept any iterable as an argument, automatically converting it
to a set before the operation. Their operator counterparts (|, &, etc.) require both operands to be
sets.
● Calling Functions: Once defined, a function is called by using its name followed by
parentheses, passing any required arguments inside the parentheses.
def greet(name):
print(f"Hello, {name}!")
greet("Alice") # Output: Hello, Alice!
○ Default Parameter Values: Parameters can have default values, making the
corresponding argument optional during the function call. Default values are
specified in the function definition using the assignment operator =.
def power(base, exponent=2): # exponent has a default value
of 2
return base ** exponent
print(power(5)) # Output: 25 (5**2)
print(power(5, 3)) # Output: 125 (5**3)
Default parameter values are evaluated only once when the function is defined.
This is important to remember when using mutable default values (like lists or
dictionaries), as modifications to such defaults persist across calls.
○ Variable-Length Arguments (*args): Allows a function to accept an arbitrary
number of positional arguments. These arguments are collected into a tuple within
the function.
def sum_all(*numbers): # numbers will be a tuple
total = 0
for num in numbers:
total += num
return total
print(sum_all(1, 2, 3)) # Output: 6
print(sum_all(10, 20, 30, 40)) # Output: 100
○ Returning Multiple Values: Python functions can return multiple values by listing
them after return, separated by commas. These values are automatically packed
into a tuple.
def get_coordinates():
return 10, 20 # Returns the tuple (10, 20)
x, y = get_coordinates() # Unpacking the tuple
print(f"x={x}, y={y}") # Output: x=10, y=20
○ Returning None:
def log_message(message):
print(message) # No explicit return
output = log_message("Processing...") # output will be None
print(output) # Output: None
○ Multi-line docstring:
def complex_function(param1, param2):
"""
A brief summary of what the function does.
More detailed explanation can follow here, describing
the algorithm, side effects, etc.
Args:
param1 (int): Description of the first parameter.
param2 (str): Description of the second parameter.
Returns:
bool: Description of the return value.
"""
# function body
return True
Well-written docstrings are crucial for code maintainability and collaboration.
● Variable Scope (LEGB Rule): As previously discussed (Section 1.2), variables defined
inside a function have local scope by default. The global keyword can be used to modify
global variables from within a function, and the nonlocal keyword can be used in nested
functions to modify variables in an enclosing function's scope.
Works cited