Module 3
Dictionaries: Dictionary operations, dictionary
methods, aliasing and copying.
Numpy: About, Shape, Slicing, masking,
Broadcasting, dtype.
Files: About files, writing our first file, reading a
file line-at-a-time, turning a file into a list of lines,
Reading the whole file at once, working with
binary files, Directories, fetching something from
the Web
Chapter 1
Dictionaries
Dictionaries
A dictionary is a compound data type in Python, similar to strings, lists, and tuples.
However, unlike those sequence types, dictionaries are mapping types.
• Sequence types (strings, lists, tuples) use integer indices (0, 1, 2 …).
• Dictionaries use keys instead of indices.
• A dictionary maps each key to a value.
In other programming languages, dictionaries are also known as:
• associative arrays
• maps
• hash maps
Properties of Dictionaries
✔ Keys
• Keys must be immutable types
(e.g., strings, numbers, tuples containing immutable items).
• Keys must be unique.
✔ Values
• Values can be any data type (strings, numbers, lists, objects, another dictionary, etc.)
• Values do not need to be unique.
✔ Syntax
A dictionary is written as:
{ key : value, key : value, ... }
Example:
{"one": "uno", "two": "dos"}
Creating a Dictionary
Method 1: Start with an empty dictionary and add items
english_spanish = {} # empty dictionary
english_spanish["one"] = "uno"
english_spanish["two"] = "dos"
After adding pairs:
print(english_spanish)
{"two": "dos", "one": "uno"}
Note: The order of items may appear different — this is normal.
Method 2: Create with key:value pairs directly
english_spanish = {"one": "uno", "two": "dos", "three": "tres"}
Accessing Values Using Keys
To get a value, simply use its key:
print(english_spanish["two"])
Output:
dos
Here:
• "two" → key
• "dos" → value
• Here, the key "two" retrieves the value "dos".
Hashing and Dictionary Ordering
Why Dictionaries Do Not Keep a Fixed Order
When you print a dictionary:
{"apples": 430, "bananas": 312, "oranges": 525, "pears": 217}
Python may show it in a different order:
{'pears': 217, 'apples': 430, 'oranges': 525, 'bananas': 312}
Reason: Hashing
Python uses an internal technique called hashing to decide:
• where each key:value pair should be stored
• how to retrieve it quickly
Hashing does not depend on the order in which you inserted items, so the printed order may
look unpredictable.
Hashing
✔ Hashing is a method that converts a key into a hash value
A hash value is a unique integer produced by Python’s hashing function.
Example:
Python internally calculates a number for "apples" like 2843920482 (example).
This number decides the storage location in memory.
✔ Why Hashing?
Because hashing allows Python to find values instantly.
• No need to search from beginning to end
• Uses the hash value to jump directly to the memory location
Why Dictionaries Are Faster Than List of Tuples
To store data like fruit stock, we can use:
(A) Dictionary
{"apples": 430, "bananas": 312, "oranges": 525}
→ Lookup is very fast (uses hashing).
(B) List of Tuples
[("apples", 430), ("bananas", 312), ("oranges", 525)]
→ Lookup is slow:
• Python must check each tuple’s first element (0th index)
• Go one by one until it finds a match
• Worst case: reach the end of the list
• Even worse: key may not be present → full scan required
Thus, dictionaries are preferred for key:value storage.
Order Does Not Matter in Dictionaries
Example dictionary:
english_spanish = {"one": "uno", "two": "dos", "three": "tres"}
We can write keys in any order because:
• dictionaries do not use positions or indices
• values are accessed using keys only
So the ordering has no meaning for dictionary operations.
Dictionaries vs Sequences
Sequence Types:
• Strings
• Lists
• Tuples
Characteristics:
• Items are arranged in order
• Accessed using integer indices
• Support slicing
(example: list[0:3])
Dictionaries:
• Items have no positional order
• Accessed using keys, not indices
• Cannot be indexed or sliced
(e.g., dict[0] → error)
Thus, a dictionary is the first compound type we study that is not a sequence.
Note
• Python dictionaries internally use hash tables.
• Hashing makes lookup extremely fast.
• Dictionary order may appear unpredictable due to hashing.
• Lists of tuples are slower for key lookup because they require linear search.
• Dictionary keys must be unique and immutable.
• Values are accessed using keys, not positions.
• Dictionaries cannot be indexed or sliced like lists or strings.
Dictionary vs. Sequence Types
Feature Strings/Lists/Tuples (Sequences) Dictionaries
Access using Integer indices Keys
Order Ordered Conceptually unordered
Slicing Yes Not possible
Speed of lookup Slow Very Fast
Key Type Not applicable Any immutable type
Value Type Homogeneous/heterogeneous Always heterogeneous
5.4.1 Dictionary Operations
Dictionaries store data as key:value pairs. Python provides several useful operations to add,
modify, delete, and inspect these pairs.
Removing a Key:Value Pair — del Statement
The del statement removes an entire entry (key and its value) from a dictionary.
Example Dictionary
inventory = {"apples": 430, "bananas": 312, "oranges": 525, "pears": 217}
print(inventory)
Output:
{'pears': 217, 'apples': 430, 'oranges': 525, 'bananas': 312}
Deleting an entry
If all bananas are sold, we delete the "bananas" item:
del inventory["bananas"]
print(inventory)
Output:
{'apples': 430, 'oranges': 525, 'pears': 217}
Trying to access a deleted key
If you now try:
print(inventory["bananas"])
Python raises an error, because "bananas" is no longer present.
Updating or Adding a Value
If we expect more bananas soon, we can add the key back or update it directly.
Setting a new value
inventory["bananas"] = 0
print(inventory)
Output:
{'pears': 217, 'apples': 430, 'oranges': 525, 'bananas': 0}
• If the key already exists, its value is updated.
• If the key does not exist, the assignment creates a new key:value pair.
Modifying an Existing Value Using +=
When new stock arrives, we can increase the quantity:
inventory["bananas"] += 200
print(inventory)
Output:
{'pears': 217, 'apples': 430, 'oranges': 525, 'bananas': 200}
Python takes the existing value of "bananas" and adds 200 to it.
Note: In your example output, "bananas" becomes 512 because the previous value was different.
The idea is the same — the value is incremented.
Using len() with Dictionaries
The len() function returns the number of key:value pairs in the dictionary.
len(inventory)
Output:
So the dictionary has 4 items in total.
Dictionary Operations
Operation Example Explanation
Deletes both key and
Remove entry del dict[key]
value
Add/update dict[key] = Creates or updates a
entry value pair
Modifies the existing
Increase value dict[key] += n
value
Dictionary Number of key:value
len(dict)
length pairs
5.4.2 Dictionary Methods
Let’s use this dictionary for all examples:
english_spanish = {
"one": "uno",
"two": "dos",
"three": "tres"
1. keys() Method
• Returns a view object containing all keys.
• This view behaves like a lazy object values are produced only when needed.
Example 1: Iterating using keys()
for key in english_spanish.keys():
print("Got key", key, "which maps to value", english_spanish[key])
Output
Got key one which maps to value uno
Got key two which maps to value dos
Got key three which maps to value tres
Example 2: Converting keys to list
keys = list(english_spanish.keys())
print(keys)
Output
['one', 'two', 'three']
Direct Iteration Over Dictionary
• Iterating over a dictionary automatically iterates over its keys.
Example
for key in english_spanish:
print("Got key", key)
Output
Got key one
Got key two
Got key three
2. values() Method
• Returns a view object of all values.
Example
print(list(english_spanish.values()))
Output
['uno', 'dos', 'tres']
3. items() Method
• Returns a view object of (key, value) pairs as tuples.
Example
print(list(english_spanish.items()))
Output
[('one', 'uno'), ('two', 'dos'), ('three', 'tres')]
Using items() in a loop
Useful when you need both key and value.
Example
for key, value in english_spanish.items():
print("Got", key, "that maps to", value)
Output
Got one that maps to uno
Got two that maps to dos
Got three that maps to tres
4. Membership Testing: in and not in
Checks only keys, not values.
Examples
print("one" in english_spanish) # True
print("six" in english_spanish) # False
print("tres" in english_spanish) # False # because 'tres' is a value, not a key
Output
True
False
False
5. KeyError on Non-existent Keys
Accessing a key not present gives an error.
Example
print(english_spanish["dog"])
Output
KeyError: 'dog'
5.4.3 Aliasing and Copying
Dictionaries are mutable, meaning their contents can be changed.
Because of this, we must be careful about aliasing.
1. Aliasing
Aliasing happens when two variables refer to the same dictionary object.
Example
opposites = {"up": "down", "right": "wrong", "yes": "no"}
alias = opposites # alias and opposites refer to the SAME dictionary
Here:
• alias → same dictionary as opposites
• Any change made using one name affects the other.
Changing the alias
alias["right"] = "left"
print(opposites["right"])
Output
left
Why?
Because alias and opposites point to the SAME object.
2. Copying a Dictionary
If you want to modify a dictionary without affecting the original, you must create a copy.
Use:
copy = [Link]() # creates a SHALLOW COPY (new dictionary)
Now:
• copy → new dictionary
• Changes to copy do NOT affect opposites
Example: modifying the copy
copy["right"] = "privilege"
print(opposites["right"])
Output
left
Original dictionary is unchanged, because copy is a separate object.
Changes Affect
Variable Relationship
Others?
Alias (same
alias = opposites Yes
object)
copy =
New dictionary No
[Link]()
5.4.4 Counting Letters Using Dictionaries
• Count how many times each letter appears in a string.
• Useful for text compression:
o Letters that appear frequently → shorter codes
o Letters that appear rarely → longer codes
letter_counts = {}
for letter in "Mississippi":
letter_counts[letter] = letter_counts.get(letter, 0) + 1
print(letter_counts)
Step 1: Initialize an empty dictionary
letter_counts = {}
• This dictionary will store letter → frequency pairs.
Step 2: Loop through the string
for letter in "Mississippi":
letter_counts[letter] = letter_counts.get(letter, 0) + 1
Explanation
1. letter_counts.get(letter, 0) → returns the current count of the letter, or 0 if the letter is not yet
in the dictionary.
2. Add 1 to update the count.
3. Repeat for every letter.
Check the frequency table
print(letter_counts)
Output:
{'M': 1, 'i': 4, 's': 4, 'p': 2}
Now the dictionary contains the frequency of each letter.
Sort the frequency table alphabetically
letter_items = list(letter_counts.items()) # convert dict_items to list
letter_items.sort() # sort by letter
print(letter_items)
Output:
[('M', 1), ('i', 4), ('p', 2), ('s', 4)]
Explanation
• letter_counts.items() → returns a dict_items view object
• list(...) → converts it into a list of tuples
• .sort() → sorts the list lexicographically by letter
formatted output
for letter, count in letter_items:
print(f"{letter} : {count}")
Output:
M:1
i:4
p:2
s:4
Chapter 2
Numpy
NumPy
Python Lists Are Not Good for Mathematical Operations
Python lists are general-purpose collections.
They are not designed for mathematical calculations like multiplication, addition, dot product,
etc.
Example:
a = [2, 3, 8]
print(2 * a)
Output:
[2, 3, 8, 2, 3, 8]
Here, the list is repeated instead of multiplying each element.
Also, multiplying a list by a float is not allowed:
2.1 * a
Output:
TypeError: can't multiply sequence by non-int of type 'float'
So, Python lists cannot perform element-wise mathematical operations.
Doing Math with Python Lists Is Not Elegant
To multiply each element by 2.1 using normal lists, we must use a loop:
values = [2, 3, 8]
result = []
for x in values:
[Link](2.1 * x)
This is lengthy, slow, and not suitable for scientific computing.
Why Use NumPy?
NumPy provides the array type, which behaves like a mathematical vector or matrix.
import numpy as np
a = [Link]([2, 3, 8])
print(2.1 * a)
Output:
array([ 4.2, 6.3, 16.8])
Advantages:
• Supports element-wise mathematical operations
• Internally optimized for speed
• Automatically converts data types (int → float) when needed
• Used for scientific and numerical computing
Note:
• numpy is usually imported as np
• [Link]() converts a Python list into a NumPy array
• NumPy automatically promotes the data type (e.g., int → float)
Element-Wise Operations with NumPy Arrays
a = [Link]([2, 3, 8])
print(a * a)
print(a ** 2)
Output:
array([ 4, 9, 64])
array([ 4, 9, 64])
NumPy performs element-wise multiplication and element-wise exponentiation.
NumPy Arrays Are Not Algebraic Vectors
In mathematics, if a is a vector, then:
• a * a → dot product
• a ** 2 → vector squared
But in NumPy:
• a * a → element-wise multiplication
• a ** 2 → element-wise square
NumPy arrays treat arithmetic operations element-by-element, not as vector algebra by default.
NumPy arrays are not true mathematical vectors. They are N-dimensional arrays that perform
element-wise operations by default:
1. Multiplication:
import numpy as np
a = [Link]([2, 3, 4])
print(a * a) # Output: [4 9 16]
Element-wise multiplication, not the dot product.
2. Exponentiation:
print(a ** 2) # Output: [4 9 16]
Not the vector “squared” in algebraic sense.
Real Vector/Matrix Algebra in NumPy
NumPy arrays perform element-wise operations by default, so for real vector or matrix algebra, you
need special functions.
1. Dot Product of Vectors
import numpy as np
a = [Link]([2, 3, 8])
result = [Link](a, a)
print(result)
Output:
77
Explanation:
• Dot product formula:
𝑎⃗ ⋅ 𝑎⃗ = 2 ∗ 2 + 3 ∗ 3 + 8 ∗ 8 = 4 + 9 + 64 = 77
• [Link]() treats arrays as vectors and applies linear algebra rules, unlike a * a which does
element-wise multiplication.
6.1 Shape of a NumPy Array
The shape of a NumPy array describes how many dimensions the array has and how many
elements are present in each dimension.
It is one of the most important properties of NumPy arrays because NumPy is designed for multi-
dimensional numerical data, such as images, matrices, and tensors.
1D array → like a simple list (row vector)
2D array → matrix (rows × columns)
Using shape Attribute
The .shape attribute returns a tuple showing the size of the array in each dimension.
Example 1: 1D Array
import numpy as np
a = [Link]([2, 3, 8])
print([Link])
Output:
(3,)
Example 2: 2D Array
b = [Link]([
[2, 3, 8],
[4, 5, 6]
])
print([Link])
Output:
(2, 3)
Meaning of Shape
• The number of values in the tuple = number of dimensions
• The values inside the tuple = size of each dimension
Shape Meaning
(5,) 1D array with 5 elements
(3, 4) 2D array with 3 rows and 4 columns
• The shape tells how many dimensions an array has and how big each dimension is.
• shape is returned as a tuple.
• 1D array example: [Link] → (3,)
• 2D array example: [Link] → (2, 3)
6.2 Slicing in NumPy
Slicing means selecting a portion of an array.
It works similar to Python lists, but becomes more powerful for multi-dimensional arrays.
Slicing 1D Arrays
For a 1D NumPy array, slicing works exactly like Python lists.
a = [Link]([2, 3, 8])
print(a[2]) # Access single element
print(a[1:]) # Slice from index 1 to end
Output
[3 8]
Slicing 2D Arrays
Consider a 2D array:
b = [Link]([
[2, 3, 8],
[4, 5, 6]
])
This array has:
• 2 rows
• 3 columns
A) Selecting a Row
b[1]
Output
array([4, 5, 6])
• b[1] selects the 1st row (remember: index starts at 0).
B) Selecting an Element from a Row
Using two steps:
b[1][2]
Or using shortcut (recommended):
b[1, 2]
Output
This means:
• Row index = 1
• Column index = 2
C) Selecting a Column
To select a column, we use : to mean “all rows”, then specify the column index.
b[:, 1]
Output
array([3, 5])
Explanation:
• : → take all rows
• 1 → take column index 1
So this gives the 1st column of the array.
1D Array Slicing
A 1D array looks like a single row:
a = [Link]([10, 20, 30, 40])
Slicing works like:
• a[start:end]
• returns a 1D slice
Examples:
a[1:3] → [20 30]
a[:2] → [10 20]
a[2:] → [30 40]
You use one set of brackets and one index position.
2D Array Slicing
A 2D array has rows and columns:
b = [Link]([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
Slicing now needs row slice and column slice:
b[row_slice , column_slice]
Examples:
Slice rows only
b[1:]
Output:
[[4 5 6]
[7 8 9]]
Slice columns only
b[:, 1:]
Output:
[[2 3]
[5 6]
[8 9]]
Slice specific block (submatrix)
b[0:2, 1:3]
Rows 0–1, columns 1–2:
[[2 3]
[5 6]]
Access single element
b[1, 2] → 6
6.3 Masking
"Masking" means selecting or modifying elements based on a condition.
A mask is simply a condition applied to an array that returns True/False values for each element.
These True/False values can then be used to select, modify, or replace specific elements of the array
— without using loops.
Creating a Mask
import numpy as np
a = [Link]([230, 10, 284, 39, 76])
cutoff = 200
print(a > cutoff)
Output:
[ True, False, True, False, False]
Step 1: Array a
[230, 10, 284, 39, 76]
This is a 1D NumPy array.
Step 2: Condition a > cutoff
cutoff = 200
a > cutoff checks each element of the array to see if it is greater than 200.
NumPy performs element-wise comparison automatically.
Step 3: Result
[ True, False, True, False, False]
Each element is tested
230 > 200 → True
10 > 200 → False
284 > 200 → True
39 > 200 → False
76 > 200 → False
This is the mask.
Using Mask to Modify Elements
We want to set all values above 200 to 0.
Instead of writing a loop, we can do:
import numpy as np
a = [Link]([23
0, 10, 284, 39, 76])
cutoff = 200
a[a > cutoff] = 0
print(a)
Output
array([0, 10, 0, 39, 76])
Explanation
• a > cutoff creates the mask
• a[a > cutoff] selects only those elements
• assigning 0 replaces them directly
This is extremely fast and efficient.
Why Masking Is Powerful (vs Loop Method)
Without masking, we must use a long and slow loop:
a = [Link]([230, 10, 284, 39, 76])
cutoff = 200
new_a = []
for x in a:
if x > cutoff:
new_a.append(0)
else:
new_a.append(x)
a = [Link](new_a)
This is:
• longer
• slower
• harder to read
• bad for large data (like images)
Masking simplifies all of this into one line.
Where Masking Is Useful
• Image processing
• Data cleaning
• Removing outliers
• Thresholding
• Applying filters
• Selecting specific elements from large arrays
For example, in a 3D color image, looping over every pixel and channel would be very complicated
— but masking makes it simple.
Note:
• Masking uses conditions to create a True/False array.
• Example mask: a > 200 → [True, False, True, ...]
• Use mask to select or replace elements:
a[a > 200] = 0
• Masking avoids loops and is extremely fast.
• Very useful for large datasets like images.
6.4 Broadcasting
Broadcasting is a powerful NumPy feature that allows operations between arrays of different
shapes.
Instead of giving errors, NumPy tries to stretch the smaller array so both arrays become compatible
for element-wise operations.
Broadcasting avoids writing loops and makes calculations fast and easy.
NumPy automatically expands smaller arrays so they can match bigger arrays during
arithmetic.
There is NO loop, NumPy does the stretching internally.
Basic Example of Broadcasting
a = [Link]([
[0, 1],
[2, 3],
[4, 5]
])
b = [Link]([10, 100])
print(a * b)
Output
array([
[ 0, 100],
[ 20, 300],
[ 40, 500]
])
Explanation
• Shape of a → (3, 2)
• Shape of b → (2,)
Because b is 1D, NumPy treats it as if it were:
[[10, 100]] # shape becomes (1, 2)
Then it stretches this across 3 rows (broadcasting):
[[10, 100],
[10, 100],
[10, 100]]
Now both arrays match shape (3, 2) and element-wise multiplication happens.
Rules of Broadcasting
Broadcasting follows two important rules:
Rule 1: A dimension can be stretched ONLY if its size is 1
If an array has a dimension of size 1, NumPy can expand it.
Example:
[Link] = (5, 4)
[Link] = (4,)
Before broadcasting, NumPy converts:
[Link] → (1, 4)
Because it adds a leading 1.
✔ This rule is ONLY to make the shapes the same length.
Rule 2: Dimensions are compared from right to left (last dimension first)
Two dimensions match if:
• They are equal, OR
• One of them is 1 (stretchable)
If neither condition is true → broadcasting fails.
Example
[Link] = (5, 4)
[Link] = (4,)
Step 1 — Apply Rule 1
Pad b:
a → (5, 4)
b → (1, 4)
Step 2 — Apply Rule 2 (compare right to left)
4 and 4 → OK
5 and 1 → OK (1 can stretch)
Broadcasting succeeds
Example 2 — Broadcasting fails
[Link] = (5, 4)
[Link] = (3,)
Step 1 — Apply Rule 1
Pad b:
a → (5, 4)
b → (1, 3)
Step 2 — Right → Left comparison:
4 and 3 → not equal
none is 1
Broadcasting fails
EXAMPLE — Broadcasting WORKS
import numpy as np
A = [Link]([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
B = [Link]([10, 20, 30]) # shape (3,)
print(A*B)
Step 1 — Rule 1: Make shapes same length
B has shape (3,)
NumPy converts it to:
B → (1, 3)
It becomes:
[[10, 20, 30]]
Step 2 — Rule 2: Compare right to left
A shape = (2, 3)
B shape = (1, 3)
Compare:
• 3 vs 3 → OK (equal)
• 2 vs 1 → OK (1 can stretch)
✔ Broadcasting possible.
Final Broadcasting Result
B stretches like this:
[[10, 20, 30],
[10, 20, 30]]
Now multiply element-wise:
A*B=
[[1*10, 2*20, 3*30],
[4*10, 5*20, 6*30]]
Result:
array([[ 10, 40, 90],
[ 40, 100, 180]])
EXAMPLE 2 :Broadcasting FAILS
Arrays:
A = [Link]([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
B = [Link]([10, 20]) # shape (2,)
print(A*B)
Step 1 — Rule 1: Make shapes same length
B becomes:
B → (1, 2)
That means NumPy sees B like this:
[[10, 20]]
Step 2 — Rule 2: Compare from right to left
A shape = (2, 3)
B shape = (1, 2)
Compare:
• 3 vs 2 → NOT equal
• neither is 1 → cannot stretch
So broadcasting fails.
ValueError: operands could not be broadcast together
How to FIX with a REAL example
Convert B to a column vector:
A = [Link]([[1, 2, 3],
[4, 5, 6]])
B = [Link]([10, 20])
B = B[:, None] # makes it shape (2, 1)
print(A*B)
Now shapes are:
A → (2, 3)
B → (2, 1)
Compare:
• 3 vs 1 → OK (1 can stretch)
• 2 vs 2 → OK (equal)
Broadcasting happens like this:
B becomes:
[[10],
[20]]
This stretches across columns:
[[10, 10, 10],
[20, 20, 20]]
Now do multiplication:
[[1*10, 2*10, 3*10],
[4*20, 5*20, 6*20]]
Result:
array([[ 10, 20, 30],
[ 80, 100, 120]])
program
import numpy as np
# Array A (2x3)
A = [Link]([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
# Array B (1D, shape 2,)
B = [Link]([10, 20]) # shape (2,)
# Convert B to a column vector (shape 2x1)
B = B[:, None]
# Multiply A and B using broadcasting
C =A* B
print("Array A:")
print(A)
print("\nArray B after converting to column vector:")
print(B)
print("\nResult of A * B with broadcasting:")
print(C)
Output:
Array A:
[[1 2 3]
[4 5 6]]
Array B after converting to column vector:
[[10]
[20]]
Result of A * B with broadcasting:
[[ 10 20 30]
[ 80 100 120]]
6.5 dtype (Data Type in NumPy)
In NumPy, dtype stands for data type. It specifies:
• The type of data stored (integer, float, etc.)
• The number of bits used to store each value
• The range of values that can be represented
Common examples:
• int8, int16, int32, int64
• uint8, uint16, uint32
• float32, float64
Integer Data Types
Bits and Value Range
Each bit can be either 0 or 1.
With n bits, there are: 𝟐𝒏 possible values
Unsigned Integers (uint)
Unsigned integers store only non-negative values.
Example: uint8
• Uses 8 bits
• Total values: 28 = 256
• Range: 𝟎 to 𝟐𝟓𝟓
Signed Integers (int)
Signed integers allow both positive and negative values.
One bit is used to represent the sign.
Example: int8
• Uses 8 bits
• Range: −𝟏𝟐𝟖 to 𝟏𝟐𝟕
Larger Integer Types
Example: int64
• Uses 64 bits
• Range: −𝟗, 𝟐𝟐𝟑, 𝟑𝟕𝟐, 𝟎𝟑𝟔, 𝟖𝟓𝟒, 𝟕𝟕𝟓, 𝟖𝟎𝟖 to + 𝟗, 𝟐𝟐𝟑, 𝟑𝟕𝟐, 𝟎𝟑𝟔, 𝟖𝟓𝟒, 𝟕𝟕𝟓, 𝟖𝟎𝟕
• This is the default integer type on most 64-bit systems.
Memory Considerations
Larger data types use more memory.
Example:
• uint8 → 1 byte per element
• int64 → 8 bytes per element
If you know your values will never exceed 100, using uint8 is more memory-efficient than int64.
Integer Overflow in NumPy
NumPy uses fixed-size integers, so it does not automatically increase precision when values
exceed the allowed range.
Example
import numpy as np
a = [Link]([200], dtype='uint8')
a+a
Output:
array([144], dtype=uint8)
Why does this happen?
• uint8 range: 0–255
• Actual result: 200 + 200 = 400
• NumPy wraps the result using modulo arithmetic:
400 𝑚𝑜𝑑 256 = 144
This behavior is called integer overflow (wrap-around).
Preventing Overflow
Use a larger dtype before performing arithmetic:
a = [Link]([200], dtype='uint16')
a+a
Output:
array([400], dtype=uint16)
Why Bigger Is Not Always Better
Although larger dtypes prevent overflow, they:
• Use more memory
• Reduce cache efficiency
• Can slow down computations on large arrays
Thus, using the largest possible dtype everywhere is inefficient.
dtype and Images
Most standard image formats (.jpg, .png) store pixel values as uint8.
Each pixel is represented as an RGB tuple:
(R, G, B)
Examples:
• (0, 0, 0) → Black
• (255, 0, 0) → Red
• (255, 255, 255) → White
Image Overflow Problem
When an image is added to itself:
image + image
Pixel values may exceed 255. Since the data type is uint8, values wrap around, producing
unexpected colors and noise instead of a brighter image.
Correct Way to Handle Image Arithmetic
Convert to a larger dtype before computation:
image = [Link](np.uint16)
result = image + image
Or clip values after computation:
result = [Link](image + image, 0, 255).astype(np.uint8)
Basic Examples of dtype in NumPy
Example 1: Checking the data type of an array
import numpy as np
a = [Link]([1, 2, 3])
print([Link])
Output:
int64
➡ By default, NumPy uses int64 on a 64-bit system.
Example 2: Creating arrays with a specific dtype
b = [Link]([1, 2, 3], dtype='int8')
print([Link])
Output:
int8
➡ Each element uses only 8 bits of memory.
Example 3: Unsigned integer (uint8)
c = [Link]([0, 100, 255], dtype='uint8')
print(c)
Output:
[ 0 100 255]
➡ uint8 allows values from 0 to 255 only.
Example 4: Integer overflow
d = [Link]([200], dtype='uint8')
print(d + d)
Output:
[144]
➡ 200 + 200 = 400, but uint8 overflows and wraps to 144.
Example 5: Avoiding overflow using a larger dtype
e = [Link]([200], dtype='uint16')
print(e + e)
Output:
[400]
➡ uint16 can store larger values, so no overflow occurs.
Example 6: Signed integer example (int8)
f = [Link]([-128, 0, 127], dtype='int8')
print(f)
Output:
[-128 0 127]
➡ int8 range is −128 to 127.
Example 7: Overflow with signed integers
g = [Link]([127], dtype='int8')
print(g + 1)
Output:
[-128]
➡ Exceeding the maximum causes wrap-around.
Example 8: Float data type
h = [Link]([1.5, 2.8, 3.1])
print([Link])
Output:
float64
➡ Decimal numbers are stored as floating-point values.
Example 9: Converting (astype) from one dtype to another
i = [Link]([10, 20, 30], dtype='int64')
j = [Link]('uint8')
print([Link])
Output:
uint8
➡ astype() changes the data type of an array.
Example 10: Image-style example (RGB values)
pixel = [Link]([255, 0, 0], dtype='uint8')
print(pixel + pixel)
Output:
[254 0 0]
➡ 255 + 255 = 510 → overflow → 254 (mod 256)
Chapter 3
About Files
7.1 About Files
When a program runs, all its data is stored in RAM (Random Access Memory).
RAM is:
• Fast
• Cheap
• Volatile → data disappears when the program closes or the computer shuts down.
To keep data permanently, we must store it in non-volatile storage, such as:
• Hard drive
• USB drive
• CD-RW
• SSD
Data stored here is saved inside files, which are named locations on the storage device.
Working with files is similar to using a notebook:
• You must open the notebook before you use it.
• After finishing, you must close it.
• While it is open, you can read or write in it.
• You can read it from start to end or jump to any section, as long as you know where to go.
Files behave the same way:
• To open a file, you give its name and say whether you want to read or write.
• Once opened, you can read from it, write to it, or move around inside it.
7.2 Writing Our First File
Let’s look at a simple Python program that writes three lines into a file:
with open("[Link]", "w") as myfile:
[Link]("My first file written from Python\n")
[Link]("\n")
[Link]("Hello, world!\n")
What happens here?
1. Opening the file
The statement:
with open("[Link]", "w") as myfile:
does two things:
• Creates a file handle → myfile
A file handle is an object Python uses to access the file.
• Opens the file in mode "w"
o "w" means write mode.
o If [Link] does not already exist → Python creates it.
o If the file exists → Python erases the old contents and writes new data.
2. Writing to the file
Each line is written using the write() method:
[Link]("text here")
• Every write() call sends text into the file.
• In larger programs, these write lines are often inside loops to write many lines.
3. Newline character The \n at the end of each string creates a new line in the file.
4. Automatically closing the file The with block ensures that the file is closed automatically when
the block ends—even if an error occurs inside the block.
(Only extreme cases like power failure can interrupt it.)
(Reference) Understanding a File Handle (Simple Analogy)
A file handle in Python is similar to a remote control for a TV.
• When you use a remote control, you press buttons (operations).
• But the actual changes happen on the TV, not on the remote.
In the same way:
• You perform operations on the file handle (like write, read, or close).
• But the real changes happen in the file stored on the disk.
Key points from the analogy
✔ The handle is not the file
Just like a remote is not the TV, a file handle is not the file itself.
It is only a tool to control the file.
✔ But we often talk about them as one
In everyday language, we might say:
• “Close the file”
• “Flip the TV channel”
even though technically, we are using the handle (or remote) to do it.
Why this analogy matters
It helps us understand that:
• A file handle is just an interface.
• The actual file lives on the disk.
• The handle provides a safe way to access and modify the file without directly touching it.
7.3 Reading a File Line-at-a-Time
Now that [Link] exists on the disk, we can open it again — this time in read mode — and read its
lines one by one.
with open("[Link]", "r") as my_new_handle:
for the_line in my_new_handle:
# Do something with the line we just read.
print(the_line, end="")
1. Opening the file for reading
open("[Link]", "r")
• "r" stands for read mode.
• The file must already exist, otherwise Python raises an error.
Inside the with block, my_new_handle becomes a file handle that lets us read the file.
2. Reading the file line by line
for the_line in my_new_handle:
This loop:
• Reads one complete line at a time
• Stops automatically when the end of the file is reached
• Makes handling large files easy (only one line is in memory at a time)
Inside the loop, you can process each line however you want.
Example: If each line had “name, email”, you could split it and send an email.
3. Why print(the_line, end="")?
Normally, print() adds its own newline at the end of what it prints.
But each line we read already includes a newline character (\n) because the for loop reads
everything up to and including the newline.
So using end="" prevents double-spacing.
Error when the file does not exist
If you try to open a file for reading that is not present:
>>> mynewhandle = open("[Link]", "r")
FileNotFoundError: [Errno 2] No such file or directory: "[Link]"
Python cannot find the file → it raises FileNotFoundError.
7.4 Turning a File into a List of Lines
Sometimes we want to read all the lines from a file into a list, so we can process them easily — sort
them, filter them, modify them, etc.
Suppose [Link] contains names and email addresses, one per line.
We want to sort these lines alphabetically and store the result in [Link].
Here’s the program:
with open("[Link]", "r") as input_file:
all_lines = input_file.readlines()
all_lines.sort()
with open("[Link]", "w") as output_file:
for line in all_lines:
output_file.write(line)
1. Reading the whole file into a list
all_lines = input_file.readlines()
• readlines() reads every line from the file.
• It returns a list of strings.
• Each string represents one line (including the newline character \n).
Example of what all_lines might look like:
["Alice <alice@[Link]>\n",
"Bob <bob@[Link]>\n",
"Zara <zara@[Link]>\n"]
2. Sorting the list
all_lines.sort()
• This sorts the list alphabetically.
• Sorting works because each element is a string.
3. Writing the sorted lines to a new file
for line in all_lines:
output_file.write(line)
• We open a new file in "w" mode.
• Then we write each sorted line to it.
Why use readlines()?
We could read each line one-by-one using a loop and manually append it to a list:
all_lines = []
for line in input_file:
all_lines.append(line)
But Python already provides the convenient method readlines(), so using it makes the program
shorter and easier.
Program for write( )
# Step 1: Create [Link] and write some names
with open("[Link]", "w") as f:
[Link]("Krishna\n")
[Link]("Amar\n")
[Link]("Zara\n")
[Link]("Balu\n")
print("[Link] created with sample names.")
# Step 2: Read all lines from [Link]
with open("[Link]", "r") as input_file:
all_lines = input_file.readlines()
# Step 3: Sort the names
all_lines.sort()
# Step 4: Write sorted names into [Link]
with open("[Link]", "w") as output_file:
for line in all_lines:
output_file.write(line)
print("Sorting completed. Check [Link]")
Program using writelines()
# Step 1: Create [Link] and write names using writelines()
names = ["Krishna\n", "Amar\n", "Zara\n", "Balu\n"]
with open("[Link]", "w") as f:
[Link](names) # write list of strings at once
print("[Link] created.")
# Step 2: Read all lines
with open("[Link]", "r") as input_file:
all_lines = input_file.readlines()
# Step 3: Sort the names
all_lines.sort()
# Step 4: Write sorted names using writelines()
with open("[Link]", "w") as output_file:
output_file.writelines(all_lines)
print("[Link] created.")
7.5 Reading the Whole File at Once
Sometimes we don't care about the file line by line.
Instead, we want the entire file as one single string so we can process it using our string methods.
For example, if we want to count words in a file, the line structure doesn’t matter.
Here’s the program:
with open("[Link]") as f:
content = [Link]()
words = [Link]()
print("There are {0} words in the file.".format(len(words)))
1. Reading the full file
content = [Link]()
• read() fetches the entire contents of the file.
• It returns one long string.
• This is useful when you want to apply string operations such as:
o split()
o replace()
o count()
o slicing
without worrying about line breaks.
2. Splitting into words
words = [Link]()
• split() (with no arguments) automatically splits the text on any whitespace:
o spaces
o tabs
o newlines
So, words become a list of individual words.
3. Counting words
len(words)
gives the total number of words in the file.
4. Default mode is "r"
Notice that in this example:
with open("[Link]") as f:
we did not specify "r".
• When the mode is omitted, Python automatically opens the file in read mode.
• So "r" is optional when reading.
Your file paths may need to be explicitly named
In the earlier examples, we assumed that [Link] is in the same folder as your Python program.
If the file is not in the same directory, Python won’t be able to find it unless you give the full path
or a relative path.
1. Full (absolute) path
A full path tells Python exactly where the file is located on your computer.
Examples:
• Windows:
"C:\\temp\\[Link]"
(We use \\ because a single backslash is a special escape character in Python strings.)
• Unix/Linux/Mac:
"/home/jimmy/[Link]"
2. Relative path
A relative path gives the location relative to your Python program.
Examples:
• "data/[Link]" → inside a folder named data
• "../[Link]" → in the parent folder
Python will follow these paths starting from the directory where your script runs.
3. We will learn more about this later
This chapter will return to the topic of paths and directories, including:
• how Python locates files
• how to change directories
• how relative and absolute paths work.
Program:
# Step 1: Create a file and write some text
with open("[Link]", "w") as f:
[Link]("Hello this is a sample file. It has some words to count.")
print("[Link] created.")
# Step 2: Read the file
with open("[Link]", "r") as f:
content = [Link]()
# Step 3: Split into words
words = [Link]()
# Step 4: Print the number of words
print("There are {0} words in the file.".format(len(words)))
Working with Binary Files
What are Binary Files?
Binary files store data in the form of bytes (0s and 1s) instead of human-readable text.
Examples of binary files:
• Images (.jpg, .png)
• Audio files (.mp3, .wav)
• Video files (.mp4)
• PDFs and executable files
➡ These files must not be opened in normal text mode.
File Modes for Binary Files
Mode Meaning
"rb" Read binary file
"wb" Write binary file
• r → read
• w → write
• b → binary
Reading a Binary File
with open("[Link]", "rb") as f:
data = [Link]()
print(data[:10])
Explanation:
• "rb" opens the file in binary read mode
• read() reads the entire file as bytes
• data[:10] prints the first 10 bytes
Writing a Binary File
with open("[Link]", "wb") as f:
[Link](data)
Explanation:
• "wb" opens the file in binary write mode
• write() writes the byte data exactly as it is
• This creates a copy of the original file
7.7 Directories
Files on a computer are stored on non-volatile storage (hard disk, SSD, USB).
These storage devices use a structure called a file system, which organizes data using:
• Files → store data
• Directories (folders) → store files and other directories
Current Directory
When a Python program runs, it has a current directory (also called working directory).
• When you create a new file, Python saves it in the current directory.
• When you open a file for reading (without giving a path), Python looks for it in the current
directory.
Example:
open("[Link]", "r") # Python looks in the current directory
Opening a File in Another Directory
To open a file somewhere else, we must give its path.
Example (Unix/Linux):
wordsfile = open("/usr/share/dict/words", "r")
wordlist = [Link]()
print(wordlist[:6])
Explanation of the path /usr/share/dict/words:
• / → top-level directory (root)
• usr → inside /
• share → inside usr
• dict → inside share
• words → the file inside dict
The program:
1. Opens the file
2. Reads all lines into a list using readlines()
3. Prints the first 6 items in the list
Windows Paths
Examples of Windows paths:
"c:/temp/[Link]"
"c:\\temp\\[Link]"
Why two backslashes?
• In Python strings, \ is an escape character (like \n for newline).
• To write a single backslash, we must use two: \\.
So the length of:
"c:/temp/[Link]"
"c:\\temp\\[Link]"
is exactly the same.
/ and \ cannot be used in filenames
• / (forward slash)
• \ (backslash)
These characters are reserved as directory separators, so they cannot appear inside actual
filenames.
Program 1 (Linux)
# Linux / Mac example from textbook
wordsfile = open("/usr/share/dict/words", "r")
wordlist = [Link]()
print(wordlist[:6])
Output on Windows
FileNotFoundError: [Errno 2] No such file or directory: '/usr/share/dict/words'
Because Windows does not have this file.
Windows Working Program (Your System)
✔ This creates your own dictionary file
✔ Reads it
✔ Prints first 6 lines (same result as the textbook)
Program 2 (Windows)
# Step 1: Create a file named [Link]
with open("[Link]", "w") as f:
[Link]("\n")
[Link]("A\n")
[Link]("A's\n")
[Link]("AOL\n")
[Link]("AOL's\n")
[Link]("Aachen\n")
[Link]("Apple\n")
[Link]("Banana\n")
# Step 2: Read the file
wordsfile = open("[Link]", "r")
wordlist = [Link]()
# Step 3: Print first 6 lines
print(wordlist[:6])
✔ Output on Windows
['\n', 'A\n', "A's\n", 'AOL\n', "AOL's\n", 'Aachen\n']
Difference Between the Two Programs
Feature Linux Program Windows Program
File path /usr/share/dict/words [Link] (created by you)
Works on Windows? No ✔ Yes
File exists already? ✔ Yes (in Linux) No → we create it
Creates and reads your own
Purpose Reads system dictionary file
dictionary
Same first 6 words (you added
Output First 6 words from system file
them manually)
Why this difference exists?
Windows does not have the /usr/share/dict/words file.
So the Linux program fails with FileNotFoundError.
To make it work, we created our own [Link] file with sample words.
If you want, I can also show:
how to read from any folder
how to join paths using [Link]()
how to check if a file exists
Using [Link] to Avoid Path Problems
Different operating systems use different directory separators:
• Unix/Linux/Mac → /
• Windows → \
Instead of worrying about this, we can let Python handle it automatically using [Link].
Example:
import os
path = [Link]("directory", "filename")
• On Unix → "directory/filename"
• On Windows → "directory\\filename"
Benefits of [Link]():
• Your code works on all operating systems
• No need to escape backslashes
• Avoids common string handling mistakes
• Makes sharing code with others easier
You can explore more functions in [Link] for:
• Getting the directory name
• Extracting the filename
• Checking if a file exists
• Working with absolute/relative paths
Note for Unix Users
The file /usr/share/dict/words usually exists on Unix-based systems.
It contains a long list of English words sorted alphabetically helpful for experiments or practice.
7.8 What About Fetching Something from the Web?
Python can read data not only from files on your computer but also from web pages on the Internet.
The Python library for this is a bit messy, but here is a simple example that downloads a file from a
URL.
1. Downloading a File Using urllib
import [Link]
url = "[Link]
destination_filename = "[Link]"
[Link](url, destination_filename)
Explanation
• [Link]()
Downloads the content from the given URL and saves it directly into a local file.
• The file will be saved in the current directory (same directory where your Python program
runs).
This single function can download any type of file from the Internet (text, PDF, images, etc.).
2. Things That Must Be Correct
Before this code works, we need to ensure:
✔ 1. The web resource must exist
If the URL is wrong or the website is down, the download will fail.
Always check the URL in your browser first.
✔ 2. You must have permission to write the destination file
If you don’t have permission (e.g., protected folder), Python will raise an error.
✔ 3. Proxy issues
Some networks (especially in colleges) use proxy servers.
In such cases, basic urllib code may fail unless extra proxy settings are configured.
For classroom or lab use, test with local or simple URLs.
✔ 4. Always verify web data before using it
Websites can change over time:
• Data may change
• The website may disappear
• A new owner may replace content with something unsafe
So programs should check whether the downloaded data is correct before using it, especially in
important applications.
2. A Better Method: Using the requests Module
requests is NOT part of the Python standard library, but it is much simpler and more powerful
than urllib.
(Install using: pip install requests)
Example: Read web content into a string
import requests
url = "[Link]
response = [Link](url)
print([Link])
Explanation
• [Link](url) → sends a request to the server
• [Link] → the content of the webpage as a single string
3. Reading Response Line-by-Line
Instead of printing everything at once, we can read it line-by-line:
import requests
url = "[Link]
response = [Link](url)
for line in response:
print(line)
What is happening here?
The response object contains:
• the downloaded text
• status code
• headers
• encoding information
• and more
Iterating over response lets you process the remote file one line at a time, just like reading a local
file.