Which of the following statements is true about Python variables and basic data types?
Anonymous Quiz
0%
A variable’s type is fixed at declaration
50%
A variable can be reassigned to a different type
0%
Variables must be declared with a type keyword
50%
Booleans are stored as strings
📘 Day 4: Strings: indexing, slicing, and common string methods
Strings in Python are immutable sequences of Unicode characters. Think of a string as a row of boxes, each holding one character, with a fixed order that never changes once the string is created. Because of this ordering, Python lets you address individual characters and extract sub‑strings using indexing and slicing.
Indexing
- Every character has a zero‑based position: the first character is index 0, the second is 1, and so on.
- Python also supports negative indices: -1 refers to the last character, -2 to the one before that, etc.
- Indexing returns a new string containing exactly one character.
Why it works: Under the hood, a string object stores its characters in a contiguous memory block. When you ask for
Slicing
- A slice is written
-
-
- The result is another string that contains the selected characters in order.
Why it works: Python builds the slice by iterating from
Common string methods – these are functions bound to a string object that return a new string (the original stays unchanged). Some frequently used ones:
-
-
-
-
-
Each method works by scanning the original string, applying its rule, and constructing a brand‑new string object. Because strings are immutable, this design avoids accidental side‑effects.
Putting it all together – a complete example
(continued below 👇)
Strings in Python are immutable sequences of Unicode characters. Think of a string as a row of boxes, each holding one character, with a fixed order that never changes once the string is created. Because of this ordering, Python lets you address individual characters and extract sub‑strings using indexing and slicing.
Indexing
- Every character has a zero‑based position: the first character is index 0, the second is 1, and so on.
- Python also supports negative indices: -1 refers to the last character, -2 to the one before that, etc.
- Indexing returns a new string containing exactly one character.
Why it works: Under the hood, a string object stores its characters in a contiguous memory block. When you ask for
s[i], Python calculates the memory offset base_address + i * size_of_character (handling negative i by adding the length). It then creates a new 1‑character string from that location.Slicing
- A slice is written
s[start:stop:step]. -
start is inclusive, stop is exclusive. If omitted, they default to the beginning (0) and the end (len(s)), respectively. -
step controls the stride; a negative step walks the string backwards. - The result is another string that contains the selected characters in order.
Why it works: Python builds the slice by iterating from
start to stop (excluding) using the given step. For each position it fetches the character (just like indexing) and appends it to a new buffer, which becomes the sliced string.Common string methods – these are functions bound to a string object that return a new string (the original stays unchanged). Some frequently used ones:
-
.upper() – returns a copy with all letters converted to uppercase. -
.lower() – returns a copy with all letters converted to lowercase. -
.replace(old, new, count=-1) – substitutes occurrences of old with new; count limits replacements. -
.split(sep=None, maxsplit=-1) – breaks the string into a list of substrings based on a separator. -
.strip(chars=None) – removes leading and trailing characters (whitespace by default). Each method works by scanning the original string, applying its rule, and constructing a brand‑new string object. Because strings are immutable, this design avoids accidental side‑effects.
Putting it all together – a complete example
# Demo: indexing, slicing, and three common methods
text = "Hello, World!" # original immutable string
# Indexing
first_char = text[0] # 'H' – start of the string
last_char = text[-1] # '!' – end of the string
# Slicing
sub = text[7:12] # 'World' – start at 7, stop before 12
reverse = text[::-1] # '!dlroW ,olleH' – step -1 walks backwards
# Methods
upper = text.upper() # 'HELLO, WORLD!'
replaced = text.replace("World", "Python") # 'Hello, Python!'
# Show everything
print("Original :", text)
print("First char:", first_char)
print("Last char :", last_char)
print("Slice [7:12]:", sub)
print("Reversed :", reverse)
print("Uppercase :", upper)
print("Replaced :", replaced)
(continued below 👇)
Walk‑through of the code
1.
2.
3.
4.
5.
6.
7. The
Understanding how these operations work—offset calculation for indexing, iteration for slicing, and construction of new buffers for methods—gives you a solid mental model. You’ll be able to predict edge cases (e.g., out‑of‑range indices raise
1.
text = "Hello, World!" creates the original string object. 2.
text[0] and text[-1] fetch single characters using positive and negative indices. 3.
text[7:12] extracts characters from position 7 up to—but not including—position 12, producing "World". 4.
text[::-1] uses a step of -1; start and stop default to the string’s bounds, so Python walks from the end to the beginning, yielding the reversed string. 5.
text.upper() iterates over every character, converting alphabetic ones to uppercase, and assembles a new string. 6.
text.replace("World", "Python") scans for the exact substring "World" and swaps it with "Python", leaving the rest untouched. 7. The
print statements display each result, confirming that the original text never changed.Understanding how these operations work—offset calculation for indexing, iteration for slicing, and construction of new buffers for methods—gives you a solid mental model. You’ll be able to predict edge cases (e.g., out‑of‑range indices raise
IndexError, slices that exceed bounds are simply clipped) and write more efficient, bug‑free string manipulations.What does the slice expression `s[2:5]` return when `s = "abcdef"`?
Anonymous Quiz
0%
"ab"
100%
"cde"
0%
"cdef"
0%
"bcd"
📘 Day 5: f-strings and string formatting
What are f‑strings?
In Python, formatted string literals (commonly called f‑strings) let you embed Python expressions directly inside string literals, using a minimal and readable syntax. They were introduced in Python 3.6 to replace older formatting methods (
Why do they work?
When the Python interpreter sees a string that starts with the letter
Basic syntax
-
-
- The surrounding text (
How is it different from older methods?
% operator – needs a separate tuple or dict, can be confusing with many placeholders.
str.format() – more flexible, but still requires you to repeat variable names or positions.
f‑strings – concise, evaluated at runtime, and can include any expression.
Embedding expressions
You can place any valid Python expression inside
-
-
Why the format spec works
After the expression, a colon
Formatting numbers, dates, and more
Explanation of each field:
-
-
-
Escaping braces
If you need a literal
Performance note
Because f‑strings are evaluated at runtime and compiled into a series of string concatenations, they are generally faster than
Complete runnable example
(continued below 👇)
What are f‑strings?
In Python, formatted string literals (commonly called f‑strings) let you embed Python expressions directly inside string literals, using a minimal and readable syntax. They were introduced in Python 3.6 to replace older formatting methods (
% operator and str.format) with something that feels natural to write and fast to execute.Why do they work?
When the Python interpreter sees a string that starts with the letter
f (or F), it parses the content looking for pairs of curly braces {}. Anything inside those braces is treated as a regular Python expression. At runtime, the expression is evaluated, converted to a string (using __format__ if a format spec is provided), and the result replaces the braces in the final output. Because the evaluation happens while the string is being built, you can use variables, function calls, or even complex expressions without any extra concatenation.Basic syntax
name = "Ada"
age = 28
msg = f"Hello, {name}! You are {age} years old."
-
f before the opening quote tells Python to treat this as an f‑string. -
{name} and {age} are replacement fields. The interpreter looks up the current value of those variables and injects them. - The surrounding text (
Hello, , ! You are , years old.) is left untouched.How is it different from older methods?
% operator – needs a separate tuple or dict, can be confusing with many placeholders.
str.format() – more flexible, but still requires you to repeat variable names or positions.
f‑strings – concise, evaluated at runtime, and can include any expression.
Embedding expressions
You can place any valid Python expression inside
{}:import math
radius = 3
area_msg = f"The area of a circle with radius {radius} is {math.pi * radius ** 2:.2f}"
-
math.pi * radius ** 2 computes the area. -
:.2f is a format specifier that tells Python to format the result as a floating‑point number with two decimal places.Why the format spec works
After the expression, a colon
: separates the value from its format spec. The spec follows the same rules as str.format(). Python calls the object's __format__ method, passing the spec string. For numbers, :.2f means fixed‑point notation with two digits after the decimal point.Formatting numbers, dates, and more
from datetime import datetime
value = 1234.5678
today = datetime(2023, 9, 1, 14, 30)
info = f"""Value: {value:,.2f}
Date : {today:%Y-%m-%d %H:%M}
Hex : {int(value):#06x}
"""
print(info)
Explanation of each field:
-
{value:,.2f} – adds commas as thousands separators and keeps two decimals. -
{today:%Y-%m-%d %H:%M} – uses datetime’s strftime‑style spec to format the date. -
{int(value):#06x} – converts value to an integer, then formats it as hexadecimal with a 0x prefix, padded to 6 characters.Escaping braces
If you need a literal
{ or } in the output, double them:msg = f"Set notation: {{1, 2, 3}}"
# Output: Set notation: {1, 2, 3}Performance note
Because f‑strings are evaluated at runtime and compiled into a series of string concatenations, they are generally faster than
str.format() and %‑formatting, especially when many placeholders are used.Complete runnable example
(continued below 👇)
#!/usr/bin/env python3
"""
Demonstrates various f‑string capabilities:
- Simple variable insertion
- Expression evaluation
- Number formatting
- Date formatting
- Hexadecimal representation
- Escaping braces
"""
from datetime import datetime
def main():
# Simple variables
user = "Grace"
tasks = 5
# Expression inside f‑string
greeting = f"Hi {user}, you have {tasks * 2} pending items."
# Number with commas and two decimals
price = 1999.99
price_msg = f"Total price: ${price:,.2f}"
# Current date and time formatting
now = datetime.now()
date_msg = f"Report generated on {now:%A, %d %B %Y at %H:%M:%S}"
# Hexadecimal with prefix and zero‑padding
checksum = 0x1a3f
hex_msg = f"Checksum: {checksum:#06x}"
# Escaping braces
set_msg = f"Example set: {{'apple', 'banana', 'cherry'}}"
# Combine everything
report = "\n".join([greeting, price_msg, date_msg, hex_msg, set_msg])
print(report)
if __name__ == "__main__":
main()
Walk‑through
1. Imports –
datetime is needed for date formatting. 2. Variables –
user, tasks, price, checksum hold basic data. 3.
greeting – {tasks * 2} multiplies tasks by 2 inside the string, demonstrating expression evaluation. 4.
price_msg – {price:,.2f} inserts price with a comma as thousand separator and exactly two decimal places. 5.
date_msg – {now:%A, %d %B %Y at %H:%M:%S} uses a strftime‑style spec to produce a human‑readable timestamp. 6.
hex_msg – {checksum:#06x} formats checksum as a zero‑padded hexadecimal (0x1a3f). 7.
set_msg – {{...}} escapes braces so the output shows literal curly braces. 8.
report – "\n".join([...]) concatenates all lines with newline characters. 9.
print(report) – outputs the fully formatted multi‑line string to the console.Running this script prints something like:
Hi Grace, you have 10 pending items.
Total price: $1,999.99
Report generated on Tuesday, 12 October 2026 at 14:23:45
Checksum: 0x1a3f
Example set: {'apple', 'banana', 'cherry'}
Each line demonstrates a different facet of f‑string power, giving you a solid foundation to apply them in real projects.
Key take‑aways
- Prefix a string with
f (or F) to enable inline expression evaluation. - Use
{} to insert variables, call functions, or run arithmetic directly. - After a colon
:, provide a format spec to control number, date, or custom formatting. - Escape literal braces by doubling them (
{{ or }}). - f‑strings are concise, readable, and usually the fastest built‑in formatting option.
Which part of an f‑string specifies how a value should be displayed, such as number of decimal places?
Anonymous Quiz
0%
The expression before the colon
50%
The format spec after the colon
0%
The variable name inside braces
50%
The leading 'f' character
📘 Day 6: Numbers and arithmetic operators
Python treats every numeric value as an object that knows its own type and how to behave with operators. Understanding the why behind numbers and arithmetic operators helps you predict results, avoid bugs, and write clearer code.
Basic numeric types
- int – whole numbers, unlimited precision (e.g.,
- float – real numbers stored in double‑precision binary format (e.g.,
- complex – numbers with a real and an imaginary part (
Each type implements the standard arithmetic operators (
1. Type promotion – if the operands differ, the smaller type is upgraded to the larger one so the operation can be performed without loss of information.
Example:
2. Operator semantics – each operator has a precise definition that the type’s implementation follows.
3. Precedence and associativity – Python evaluates operators in a fixed order (e.g.,
The arithmetic operators in detail
-
-
-
-
-
-
-
Why does
Python defines floor division as:
A complete, runnable example
Below is a short script that touches every numeric type and each arithmetic operator. Run it line‑by‑line to see the output and understand what’s happening.
Line‑by‑line walk‑through
(continued below 👇)
Python treats every numeric value as an object that knows its own type and how to behave with operators. Understanding the why behind numbers and arithmetic operators helps you predict results, avoid bugs, and write clearer code.
Basic numeric types
- int – whole numbers, unlimited precision (e.g.,
-3, 0, 42). - float – real numbers stored in double‑precision binary format (e.g.,
3.14, -0.001). - complex – numbers with a real and an imaginary part (
2 + 3j). Each type implements the standard arithmetic operators (
+, -, *, /, //, %, **). When you combine two numbers, Python follows a set of rules:1. Type promotion – if the operands differ, the smaller type is upgraded to the larger one so the operation can be performed without loss of information.
Example:
int + float → both become float. 2. Operator semantics – each operator has a precise definition that the type’s implementation follows.
3. Precedence and associativity – Python evaluates operators in a fixed order (e.g.,
** before *, /, //, %, which are before + and -). Parentheses () can override this order.The arithmetic operators in detail
-
+ (addition) – adds two numbers. For int and float it’s straightforward; for complex it adds both real and imaginary parts. -
- (subtraction) – subtracts right operand from left. -
* (multiplication) – multiplies. With complex, it follows the algebraic rule (a+bj)*(c+dj). -
/ (true division) – always returns a float, even if both operands are int. Python uses real‑number division, not integer truncation. -
// (floor division) – divides then floors the result to the nearest lower integer (or lower whole number for negatives). The result type matches the left operand’s type when possible. -
% (modulo) – returns the remainder after floor division. It satisfies the identity a == (a // b) * b + (a % b). -
** (exponentiation) – raises left operand to the power of right operand. Supports fractional exponents, which produce a float. Why does
// floor the result?Python defines floor division as:
a // b == math.floor(a / b)
math.floor always moves toward negative infinity. This behaviour is consistent with the modulo operator’s identity above, ensuring that (a // b) * b + (a % b) exactly reconstructs the original a.A complete, runnable example
Below is a short script that touches every numeric type and each arithmetic operator. Run it line‑by‑line to see the output and understand what’s happening.
# Numbers and arithmetic operators demo
# 1️⃣ Define values of different numeric types
a = 7 # int: whole number
b = 3.5 # float: decimal number
c = 2 + 3j # complex: real + imag*j
# 2️⃣ Basic operations
add = a + b # int + float → float
sub = a - b # subtraction → float
mul = a * b # multiplication → float
true_div = a / b # true division → float
floor_div = a // b # floor division → int (floors 2.0)
mod = a % b # remainder after floor division → float
exp = a ** 2 # exponentiation → int
complex_mul = c * (1 - 1j) # complex multiplication
# 3️⃣ Show results
print("a (int) =", a)
print("b (float) =", b)
print("c (complex) =", c)
print("a + b =", add) # 10.5
print("a - b =", sub) # 3.5
print("a * b =", mul) # 24.5
print("a / b =", true_div) # 2.0
print("a // b =", floor_div) # 2
print("a % b =", mod) # 0.5
print("a ** 2 =", exp) # 49
print("c * (1-1j) =", complex_mul) # (5+1j)
Line‑by‑line walk‑through
(continued below 👇)
-
-
-
-
-
-
-
-
-
Running the script prints each intermediate value, confirming how Python’s numeric tower and operator definitions work together.
Key take‑aways
- Type promotion guarantees operations don’t lose precision.
-
- Operator precedence follows the standard mathematical hierarchy; use parentheses to make intent explicit.
- Complex numbers behave algebraically, and the same operators apply.
Understanding these fundamentals makes higher‑level numeric work—statistics, scientific computing, graphics—much more predictable.
a = 7 creates an int object holding the value 7. -
b = 3.5 creates a float object. -
c = 2 + 3j creates a complex object where 2 is the real part and 3j is the imaginary part. -
add = a + b triggers type promotion: a is promoted to float, then addition yields 10.5. -
true_div = a / b always returns a float; even though 7 / 3.5 is mathematically 2, the result is 2.0. -
floor_div = a // b computes 7 / 3.5 = 2.0 then floors it to 2. Because the left operand is an int, the result stays an int. -
mod = a % b satisfies 7 == (7 // 3.5) * 3.5 + (7 % 3.5), so 7 % 3.5 equals 0.5. -
exp = a ** 2 squares 7, returning an int (49). -
complex_mul = c * (1 - 1j) uses the algebraic rule (a+bj)*(c+dj) = (ac‑bd) + (ad+bc)j. The result is (5+1j). Running the script prints each intermediate value, confirming how Python’s numeric tower and operator definitions work together.
Key take‑aways
- Type promotion guarantees operations don’t lose precision.
-
/ vs //: true division always yields a float; floor division discards the fractional part toward negative infinity. - Operator precedence follows the standard mathematical hierarchy; use parentheses to make intent explicit.
- Complex numbers behave algebraically, and the same operators apply.
Understanding these fundamentals makes higher‑level numeric work—statistics, scientific computing, graphics—much more predictable.
In Python, what is the result of the expression `7 // 3`?
Anonymous Quiz
33%
2
0%
2.3333333333333335
0%
3
67%
1
📘 Day 7: input() and type conversion
Understanding
When you write a program that interacts with a user, the most common way to get data is the built‑in
But what
1️⃣ What
1. Displays a prompt – the string you pass (
2. Waits for the user to type – everything the user types until they press Enter is captured.
3. Returns a
Why a string?
The underlying operating system sends the typed characters as a sequence of bytes. Python decodes those bytes using the default text encoding (usually UTF‑8) and builds a string object. This design keeps
2️⃣ Why you need type conversion
Most programs need numbers, dates, booleans, or other non‑text data. If you try to use the raw string as a number, Python will raise a
Here
3️⃣ The basic conversion functions
Function — Purpose — Typical error
Why these work: Each function parses the characters of the string according to the rules of the target numeric format. If the pattern doesn’t match, Python raises a
4️⃣ Safe conversion with error handling
A robust script never assumes the user will type the correct format. Use
What happens step‑by‑step
1.
2.
3. If parsing succeeds, we get an int stored in
4. If parsing fails, Python jumps to the
5️⃣ Full example – a simple calculator
Below is a complete, runnable script that demonstrates prompting, type conversion, error handling, and using the converted values:
(continued below 👇)
Understanding
input() and Type Conversion in PythonWhen you write a program that interacts with a user, the most common way to get data is the built‑in
input() function. It looks simple:name = input("Enter your name: ")But what
input() actually returns, and why we often need to convert that value, is a fundamental concept that underpins every interactive script.1️⃣ What
input() really does 1. Displays a prompt – the string you pass (
"Enter your name: "). 2. Waits for the user to type – everything the user types until they press Enter is captured.
3. Returns a
str object – even if the user typed digits, the result is still a string.Why a string?
The underlying operating system sends the typed characters as a sequence of bytes. Python decodes those bytes using the default text encoding (usually UTF‑8) and builds a string object. This design keeps
input() simple and language‑agnostic: the function never guesses the intended data type.2️⃣ Why you need type conversion
Most programs need numbers, dates, booleans, or other non‑text data. If you try to use the raw string as a number, Python will raise a
TypeError or give you unexpected results:age = input("Enter your age: ")
print(age + 10) # ❌ TypeError: can only concatenate str (not "int") to strHere
age is a string like "27". Adding 10 to a string would mean concatenation, not arithmetic. To perform math we must turn the string into an integer (or a float if decimals are allowed).3️⃣ The basic conversion functions
Function — Purpose — Typical error
int(s) — Convert string s to an integer — ValueError if s isn’t a valid integer ("12a" )float(s) — Convert string s to a floating‑point number — ValueError if s isn’t a valid float ("abc" )bool(s) — Convert string s to a boolean (False only if s is empty or "0" ) — Rarely raises, but truthiness may be confusingWhy these work: Each function parses the characters of the string according to the rules of the target numeric format. If the pattern doesn’t match, Python raises a
ValueError to signal that conversion is impossible.4️⃣ Safe conversion with error handling
A robust script never assumes the user will type the correct format. Use
try / except to catch conversion failures and react gracefully:while True:
raw = input("Enter a positive integer: ")
try:
number = int(raw)
if number > 0:
break # valid input, exit loop
else:
print("⚠️ Must be greater than zero.")
except ValueError:
print("⚠️ That's not an integer. Try again.")
What happens step‑by‑step
1.
input() returns whatever the user typed (raw). 2.
int(raw) attempts to parse it. 3. If parsing succeeds, we get an int stored in
number. 4. If parsing fails, Python jumps to the
except block, where we inform the user and repeat.5️⃣ Full example – a simple calculator
Below is a complete, runnable script that demonstrates prompting, type conversion, error handling, and using the converted values:
(continued below 👇)
# Simple calculator: adds two numbers entered by the user
def get_number(prompt: str) -> float:
"""Repeatedly ask for a number until the user provides a valid float."""
while True:
raw = input(prompt)
try:
# Convert the raw string to a floating‑point number
return float(raw)
except ValueError:
# Inform the user why the conversion failed
print("❌ Invalid number, please type digits like 3.14 or 42.")
def main():
print("🧮 Welcome to the Mini Calculator!")
# Get the first operand
a = get_number("Enter the first number: ")
# Get the second operand
b = get_number("Enter the second number: ")
# Perform addition – now we can use '+' as arithmetic because a & b are floats
result = a + b
# Show the result, formatted to two decimal places
print(f"✅ The sum of {a:.2f} and {b:.2f} is {result:.2f}")
if __name__ == "__main__":
main()
Walk‑through
-
def get_number(prompt: str) -> float: – a helper that encapsulates the input‑convert‑retry pattern. -
raw = input(prompt) – captures the user’s raw text. -
float(raw) – attempts to turn that text into a numeric value (float). -
except ValueError: – catches the situation where the text isn’t a valid number and prompts again. -
a = get_number(...) & b = get_number(...) – we now have two float objects, ready for arithmetic. -
result = a + b – normal numeric addition works because both operands are numbers, not strings. -
print(f"...") – formatted output shows the numbers with two decimal places, demonstrating that they are indeed numeric.6️⃣ Common pitfalls & best practices
- Never trust raw input – always validate and convert.
- Prefer
float for generic numeric entry – it accepts both integer‑style ("5") and decimal ("5.0"). - Use
strip() if you want to ignore surrounding whitespace: raw = input(...).strip(). - Remember that
bool("False") is True – the string is non‑empty, so it evaluates to True. Use explicit checks (raw.lower() in ("true", "yes", "1")). - Separate concerns – keep prompting/validation in its own function (as shown) to keep the main logic clean.
7️⃣ Quick bolded checklist
-
input() always returns a str. - Convert with
int(), float(), or custom parsing. - Wrap conversions in
try/except to handle bad data. - Validate ranges (e.g., positive numbers) after conversion.
- Never mix raw strings with numeric operators.
Mastering
input() and type conversion is the first step toward building reliable, user‑friendly command‑line applications. Once you internalize the why behind each step, you’ll write safer code with confidence.What does the built‑in `input()` function return in Python?
Anonymous Quiz
0%
An integer
100%
A string
0%
A list
0%
A boolean
📘 Day 8: Lists: creating, indexing, slicing, and common methods
Lists are one of Python’s most versatile built‑in data structures. Think of a list as an ordered collection of items that can be of any type—numbers, strings, even other lists. Because a list is mutable, you can change its contents after creation, which is why understanding how they work is essential for writing flexible code.
Creating a list
- Use square brackets
- An empty list is just
- You can also build a list from an iterable with the
Why does
Indexing – accessing a single element
- Positive indices start at
- Negative indices start at
- An out‑of‑range index raises
Why does
Python translates a negative index
Slicing – extracting a sub‑list
Syntax:
-
- Omitting
-
Why does slicing return a new list?
When you slice, Python creates a shallow copy containing references to the original elements. This protects the original list from accidental modification via the slice.
Common list methods – mutating the list in place
Bolded list of the most used methods
-
-
-
-
-
-
-
-
-
-
(continued below 👇)
Lists are one of Python’s most versatile built‑in data structures. Think of a list as an ordered collection of items that can be of any type—numbers, strings, even other lists. Because a list is mutable, you can change its contents after creation, which is why understanding how they work is essential for writing flexible code.
Creating a list
- Use square brackets
[] and separate items with commas. - An empty list is just
[]. - You can also build a list from an iterable with the
list() constructor.# Different ways to create lists
numbers = [1, 2, 3, 4] # literal syntax
empty = [] # an empty list
mixed = ['apple', 3, 4.5, True] # any mix of types
from_range = list(range(5)) # converts a range into a list
Why does
list(range(5)) work? range(5) produces a range object that lazily generates numbers 0‑4. The list() constructor iterates over that object, collecting each number into a new list. This illustrates that any iterable can be turned into a list.Indexing – accessing a single element
- Positive indices start at
0 (first element). - Negative indices start at
-1 (last element). - An out‑of‑range index raises
IndexError.fruits = ['apple', 'banana', 'cherry']
first = fruits[0] # 'apple'
last = fruits[-1] # 'cherry'
Why does
fruits[-1] give the last item? Python translates a negative index
-n to len(fruits) - n. So -1 becomes 3 - 1 = 2, which points to the element at position 2.Slicing – extracting a sub‑list
Syntax:
my_list[start:stop:step] -
start is inclusive, stop is exclusive. - Omitting
start defaults to the beginning; omitting stop defaults to the end. -
step controls the stride; a negative step reverses direction.nums = list(range(10)) # [0,1,2,3,4,5,6,7,8,9]
first_three = nums[0:3] # [0, 1, 2]
middle = nums[3:7] # [3, 4, 5, 6]
every_second = nums[::2] # [0, 2, 4, 6, 8]
reverse = nums[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Why does slicing return a new list?
When you slice, Python creates a shallow copy containing references to the original elements. This protects the original list from accidental modification via the slice.
Common list methods – mutating the list in place
Bolded list of the most used methods
-
append(item) – adds item to the end. -
extend(iterable) – concatenates another iterable’s items. -
insert(index, item) – inserts item before index. -
remove(value) – deletes the first occurrence of value. -
pop([index]) – removes and returns element at index (default last). -
clear() – empties the list completely. -
sort(key=None, reverse=False) – sorts in place. -
reverse() – reverses order in place. -
count(value) – counts occurrences of value. -
index(value, start=0, stop=len(list)) – finds first index of value.# Demonstrating several list methods in one script
shopping = ['bread', 'milk']
shopping.append('eggs') # ['bread', 'milk', 'eggs']
shopping.extend(['cheese', 'butter']) # ['bread', 'milk', 'eggs', 'cheese', 'butter']
shopping.insert(1, 'apples') # ['bread', 'apples', 'milk', 'eggs', 'cheese', 'butter']
shopping.remove('milk') # removes first 'milk'
last_item = shopping.pop() # removes and returns 'butter'
shopping.sort() # alphabetical order
shopping.reverse() # now descending order
count_apples = shopping.count('apples')
pos_apples = shopping.index('apples')
print('Final list:', shopping)
print('Removed item:', last_item)
print('Apples count:', count_apples, 'at position', pos_apples)
(continued below 👇)
❤1
Walk‑through of the example
1.
2.
3.
4.
5.
6.
7.
8.
9.
10. The final
Understanding why each method behaves as it does helps you predict side‑effects—crucial when working with mutable objects in larger programs.
Key takeaways
- Lists are ordered, mutable, and can hold heterogeneous items.
- Indexing uses zero‑based positions; negative indices count from the end.
- Slicing produces a shallow copy, letting you safely work on sub‑segments.
- The built‑in methods modify the list in place (except those that return a value, like
Practice by experimenting: change the
1.
shopping = ['bread', 'milk'] creates a list with two strings. 2.
append('eggs') adds 'eggs' to the tail, expanding the list in place. 3.
extend([...]) takes each element from the new iterable and appends them sequentially—more efficient than multiple append calls. 4.
insert(1, 'apples') shifts existing items rightward, placing 'apples' at index 1. 5.
remove('milk') searches left‑to‑right for the first 'milk' and deletes it. 6.
pop() without an argument defaults to the last element; it also returns that element so we can store it in last_item. 7.
sort() rearranges the list alphabetically because all elements are strings. 8.
reverse() flips the order, demonstrating that reverse works on the already‑sorted list. 9.
count('apples') and index('apples') illustrate read‑only queries that do not modify the list. 10. The final
print statements show the resulting list and the values we captured.Understanding why each method behaves as it does helps you predict side‑effects—crucial when working with mutable objects in larger programs.
Key takeaways
- Lists are ordered, mutable, and can hold heterogeneous items.
- Indexing uses zero‑based positions; negative indices count from the end.
- Slicing produces a shallow copy, letting you safely work on sub‑segments.
- The built‑in methods modify the list in place (except those that return a value, like
pop). Practice by experimenting: change the
step in a slice, combine extend with other iterables, and observe how sort behaves with numbers versus strings. Mastery of lists unlocks the majority of everyday Python data handling.Which expression returns a new list containing the last three elements of `my_list` in reverse order?
Anonymous Quiz
67%
my_list[-3:]
0%
my_list[-3:][::-1]
33%
my_list[::-3]
0%
my_list[-1:-4]
📘 Day 9: Tuples and when to use them instead of lists
Tuples are one of Python’s built‑in sequence types. Like lists, they store an ordered collection of items, but they differ in a crucial way: a tuple’s contents cannot be changed after creation. This immutability gives tuples distinct advantages and influences when you should pick them over lists.
How a tuple is created
*
* Inside we place any objects, separated by commas.
* Even a single element needs a trailing comma:
Why immutability matters
* Safety – Once a tuple is built, its items stay the same. Accidentally modifying data in a large codebase becomes impossible, which prevents bugs.
* Hashability – Immutable objects can be hashed, so a tuple can be used as a dictionary key or placed inside a set; lists cannot.
* Performance – Because Python knows a tuple won’t change, it can store it more compactly and access items slightly faster than a list.
When to prefer a tuple over a list
Use a tuple when:
- The collection represents a fixed collection of items (e.g., coordinates, RGB values, database rows).
- You need to protect data from accidental modification.
- You want to use the collection as a key in a dict or store it in a set.
- You care about the tiny speed/ memory edge for read‑only sequences.
Use a list when:
- The size or contents will change (add, remove, reorder).
- You need list‑specific methods like
- The collection is a dynamic container such as a queue, stack, or buffer.
Bolded comparison list
- Mutability – List: mutable | Tuple: immutable
- Typical use – List: dynamic collections | Tuple: fixed collections
- Hashable – List: no | Tuple: yes (if all elements are hashable)
- Methods – List: many (append, pop…) | Tuple: few (count, index)
Real‑world example: Using a tuple as a dictionary key
Walk‑through
1. Define the dictionary – Keys are tuples
2. Assign values – The values are the populations (integers).
3. Access a value –
4. Print – The
If we had tried to use a list as a key (
More subtle benefits
- Function return values – Functions that need to return multiple related values often use a tuple, e.g.,
- Unpacking – Tuples support clean unpacking:
Because the size is fixed, a mismatch in the number of variables raises a clear error, catching bugs early.
Converting between lists and tuples
The conversion functions
TL;DR
(continued below 👇)
Tuples are one of Python’s built‑in sequence types. Like lists, they store an ordered collection of items, but they differ in a crucial way: a tuple’s contents cannot be changed after creation. This immutability gives tuples distinct advantages and influences when you should pick them over lists.
How a tuple is created
# A tuple containing three different types
coordinates = (12.5, -3, "point A")
*
( and ) delimit the tuple. * Inside we place any objects, separated by commas.
* Even a single element needs a trailing comma:
single = (42,).Why immutability matters
* Safety – Once a tuple is built, its items stay the same. Accidentally modifying data in a large codebase becomes impossible, which prevents bugs.
* Hashability – Immutable objects can be hashed, so a tuple can be used as a dictionary key or placed inside a set; lists cannot.
* Performance – Because Python knows a tuple won’t change, it can store it more compactly and access items slightly faster than a list.
When to prefer a tuple over a list
Use a tuple when:
- The collection represents a fixed collection of items (e.g., coordinates, RGB values, database rows).
- You need to protect data from accidental modification.
- You want to use the collection as a key in a dict or store it in a set.
- You care about the tiny speed/ memory edge for read‑only sequences.
Use a list when:
- The size or contents will change (add, remove, reorder).
- You need list‑specific methods like
append, extend, pop, remove. - The collection is a dynamic container such as a queue, stack, or buffer.
Bolded comparison list
- Mutability – List: mutable | Tuple: immutable
- Typical use – List: dynamic collections | Tuple: fixed collections
- Hashable – List: no | Tuple: yes (if all elements are hashable)
- Methods – List: many (append, pop…) | Tuple: few (count, index)
Real‑world example: Using a tuple as a dictionary key
# Suppose we store the population of several cities.
# The (country_code, city_name) pair uniquely identifies a city.
city_population = {
("US", "New York"): 8_398_748,
("JP", "Tokyo"): 13_960_236,
("FR", "Paris"): 2_148_271,
}
# Retrieve the population of Tokyo
print(city_population[("JP", "Tokyo")])
Walk‑through
1. Define the dictionary – Keys are tuples
("US", "New York") etc. Because tuples are immutable, Python can hash them and use them as keys.2. Assign values – The values are the populations (integers).
3. Access a value –
city_population[("JP", "Tokyo")] looks up the exact tuple key and returns the associated number.4. Print – The
print call outputs 13960236.If we had tried to use a list as a key (
["JP", "Tokyo"]), Python would raise a TypeError: unhashable type: 'list'.More subtle benefits
- Function return values – Functions that need to return multiple related values often use a tuple, e.g.,
return min_val, max_val. The caller receives a fixed‑size, immutable package.- Unpacking – Tuples support clean unpacking:
x, y, label = coordinates # x=12.5, y=-3, label='point A'
Because the size is fixed, a mismatch in the number of variables raises a clear error, catching bugs early.
Converting between lists and tuples
my_list = [1, 2, 3]
my_tuple = tuple(my_list) # list → tuple
back_to_list = list(my_tuple) # tuple → list
The conversion functions
tuple() and list() simply copy the elements into a new container of the desired type. No data is lost; only mutability changes.TL;DR
(continued below 👇)
- Tuples = immutable, hashable, slightly faster, ideal for fixed collections.
- Lists = mutable, feature‑rich, meant for data that changes.
Choose the type that matches the intent of your data, not just the current shape.
- Lists = mutable, feature‑rich, meant for data that changes.
Choose the type that matches the intent of your data, not just the current shape.
Which of the following is a valid reason to choose a tuple over a list in Python?
Anonymous Quiz
50%
It allows item modification
0%
It can be used as a dict key
50%
It supports .append() method
0%
It automatically sorts its items