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.
Dictionary
A dictionary in Python is a built-in data type that stores data as key-value pairs. Unlike lists
or tuples, which are indexed by numbers, dictionaries use keys to access their values.
Keys must be immutable (like strings, numbers, or tuples).
Values can be of any data type (numbers, strings, lists, other dictionaries, etc.).
Dictionaries are unordered, meaning the items do not have a fixed position.
Dictionaries are Python’s built-in mapping type, unlike sequences (strings, lists, tuples)
that use integer indices.
They map keys to values, where keys are immutable types and values can be of any
type (heterogeneous).
In other languages, dictionaries are often called associative arrays.
Creating a dictionary:
english_spanish = {} # empty dictionary
english_spanish["one"] = "uno" # add key:value pair
english_spanish["two"] = "dos"
print(english_spanish) # Output: {'one': 'uno', 'two': 'dos'}
Key:value pairs are separated by commas, with a colon between each key and its value.
Hashing in Python
Hashing in Python refers to the technique used to store and access data quickly in data
structures like dictionaries and sets.
When you use a key in a dictionary, Python applies a hash function to the key.
This hash function converts the key into a unique integer called a hash value, which
determines where the value is stored in memory.
Because of hashing, accessing a value by its key is very fast, even in large dictionaries.
Hashing also means that dictionaries are unordered — the order of key:value pairs is
unpredictable.
Dictionaries are unordered: The order of key:value pairs may seem unpredictable.
Python uses hashing, a fast algorithm, to decide where each key:value pair is stored.
Why not use a list of tuples?
o Example of a list of tuples:
o [('apples', 430), ('bananas', 312), ('oranges', 525), ('pears', 217)]
o Searching for a key in a list of tuples is slow, because Python would need to
check each tuple one by one.
o Dictionaries are much faster because hashing allows direct access to the value
using the key.
Creating a dictionary with key:value pairs
english_spanish = {"one": "uno", "two": "dos", "three": "tres"}
print(english_spanish["two"]) # Output: 'dos'
Order of key:value pairs doesn’t matter.
Values are accessed using keys, not indices.
Note:
Lists, tuples, and strings are sequences (ordered).
Dictionaries are not sequences; you cannot index or slice them.
Hashing ensures fast access to values via keys.
Dictionary operations
The del statement can be used to remove a key:value pair from a dictionary.
Example:
inventory = {"apples": 430, "bananas": 312, "oranges": 525, "pears": 217}
# Remove bananas
del inventory["bananas"]
print(inventory)
# Output: {'apples': 430, 'oranges': 525, 'pears': 217}
Accessing a deleted key will result in an error.
Updating values
You can change the value of an existing key:
# Set bananas to 0 if out of stock
inventory["bananas"] = 0
print(inventory)
# Output: {'apples': 430, 'oranges': 525, 'pears': 217, 'bananas': 0}
# Increase stock when new shipment arrives
inventory["bananas"] += 200
print(inventory)
# Output: {'apples': 430, 'oranges': 525, 'pears': 217, 'bananas': 200}
Other useful operations
len() returns the number of key:value pairs in a dictionary:
len(inventory) # Output: 4
Dictionary methods
1. keys() method
Returns a view object of all keys in the dictionary.
Can iterate over it or convert it to a list:
english_spanish = {"one": "uno", "two": "dos", "three": "tres"}
# Iterate over keys
for key in english_spanish.keys():
print(key, "->", english_spanish[key])
# Convert keys to list
keys_list = list(english_spanish.keys())
print(keys_list)
You can omit .keys() in a loop — iterating a dictionary defaults to keys:
for key in english_spanish:
print(key)
2. values() method
Returns a view of all values in the dictionary.
print(list(english_spanish.values()))
# Output: ['uno', 'dos', 'tres']
3. items() method
Returns a view of key:value pairs as tuples.
print(list(english_spanish.items()))
# Output: [('one', 'uno'), ('two', 'dos'), ('three', 'tres')]
# Iterate over both key and value
for key, value in english_spanish.items():
print(key, "maps to", value)
4. Membership testing
Use in and not in to check if a key exists in the dictionary:
"one" in english_spanish # True
"six" in english_spanish # False
Note: in only checks keys, not values. Accessing a missing key raises a KeyError:
english_spanish["dog"] # KeyError
Aliasing and Copying in Dictionaries:
Aliasing
Dictionaries in Python are mutable.
Aliasing happens when two variables refer to the same dictionary.
Any change made through one variable affects the other, because they point to the same
object.
Example:
opposites = {"up": "down", "left": "right"}
alias_dict = opposites # aliasing
alias_dict["up"] = "DOWN"
print(opposites)
# Output: {'up': 'DOWN', 'left': 'right'}
Changing alias_dict also changed opposites because they are the same object.
Copying
To avoid aliasing, you can make a copy of the dictionary using the .copy() method.
This creates a separate dictionary that can be modified independently.
Example:
opposites = {"up": "down", "left": "right"}
copy_dict = [Link]() # separate copy
copy_dict["up"] = "UPPER"
print(opposites)
# Output: {'up': 'down', 'left': 'right'}
print(copy_dict)
# Output: {'up': 'UPPER', 'left': 'right'}
Dictionary: A collection of key:value pairs that maps from keys to values. The
keys can be any immutable value, and the associated value can be of any type.
Immutable data value: A data value which cannot be modified. Assignments to
elements or slices (sub-parts) of immutable values cause a runtime error.
Key: A data item that is mapped to a value in a dictionary. Keys are used to look
up values in a dictionary. Each key must be unique across the dictionary.
key:value pair: One of the pairs of items in a dictionary. Values are looked up in a
dictionary by key.
Numpy
NumPy (Numerical Python) is a Python library used to perform mathematical, scientific,
and numerical operations efficiently.
It provides a special data type called ndarray (NumPy array), which behaves like a
mathematical array or matrix.
Why We Use NumPy in Python
1. Python lists are not suitable for mathematical operations
Example:
a = [2, 3, 8]
2*a
Output:
[2, 3, 8, 2, 3, 8]
Multiplying a list by an integer only repeats the list, it does NOT do mathematical
multiplication.
If you try multiplying by a float:
2.1 * a
You get:
TypeError: can't multiply sequence by non-int of type 'float'
So lists:
cannot do element-wise multiplication
do not support float multiplication
are not mathematical objects
To do element-wise multiplication using lists, we must use a loop:
values = [2, 3, 8]
result = []
for x in values:
[Link](2.1 * x)
This is slow and not elegant.
NumPy solves this problem
NumPy provides a special type called ndarray which behaves like a mathematical array or
vector.
Example:
import numpy as np
a = [Link]([2, 3, 8])
2.1 * a
Output:
array([4.2, 6.3, 16.8])
Important points:
numpy is usually imported as np.
[Link]() converts a Python list into a NumPy array.
Even though [2, 3, 8] contains integers, NumPy converts them to floats automatically
when needed.
Element-wise operations
Example:
a*a
Output:
array([4, 9, 64])
Or:
a**2
Output:
array([4, 9, 64])
NumPy performs element-wise operations by default.
Why is a**2 not a dot product?
Because:
NumPy arrays are not algebraic vectors
Basic arithmetic is element-wise, NOT vector multiplication
To compute the dot product, use:
[Link](a, a)
Output:
77
For 2D arrays, [Link] performs matrix multiplication.
Other algebra functions exist:
[Link]() – cross product
[Link]() – outer product
many more in NumPy's linear algebra module
Bottom Line
**Python lists cannot perform mathematical operations properly.
NumPy arrays can.**
Use NumPy when you want:
fast mathematical calculations
vector or matrix operations
scientific or data-science computations
clean and elegant code
Shape in NumPy
The shape of a NumPy array tells you how many elements it has along each dimension.
A dimension (also called axis) is like a direction in the array:
1D array → only 1 direction
2D array → rows and columns
3D array → depth × rows × columns (e.g., color images)
Example 1: 1D Array
import numpy as np
a = [Link]([2, 3, 8])
[Link]
Output:
(3,)
Meaning:
This array has 3 elements in one dimension.
So the shape is (3,)
Example 2: 2D Array (Matrix)
b = [Link]([
[2, 3, 8],
[4, 5, 6]
])
[Link]
Output:
(2, 3)
Meaning:
2 rows
3 columns
Shape: (rows, columns)
Slicing in NumPy
Slicing in NumPy is the process of selecting a portion of an array using index ranges.
It allows you to extract rows, columns, or specific sub-arrays from a NumPy array
without copying the data.
General Slice Syntax
array[start : stop : step]
start → index where slice begins
stop → index where slice ends (not included)
step → jump size
Examples
1D Array
a[1:4]
Selects elements from index 1 to 3.
2D Array
b[ :, 1 ]
Selects all rows, column 1.
b[1, :]
Selects row 1, all columns.
b[0:2, 1:3]
Selects a sub-matrix.
Consider this array:
b = [Link]([
[2, 3, 8],
[4, 5, 6],
])
This is a 2D array with shape (2, 3)
→ 2 rows and 3 columns.
1. Getting a full row
b[1]
Output:
array([4, 5, 6])
This retrieves row 1 (remember: counting starts at 0).
2. Getting a single element
Two methods:
Method 1 (normal Python):
b[1][2]
Method 2 (NumPy shorthand):
b[1, 2]
Both output:
6
3. Getting a full column
This is where NumPy slicing is powerful.
To get all rows, use :
To get column 1, use index 1.
b[:, 1]
Output:
array([3, 5])
Masking in NumPy
Masking in NumPy is a technique where you create a Boolean array (True/False values)
based on some condition, and then use that Boolean array to select or modify specific
elements of another NumPy array.
OR
Masking in NumPy is selecting or modifying array elements using a Boolean condition,
where NumPy applies operations only to values for which the mask is True.
How Masking Works
1. Apply a condition on the array
2. a > cutoff
This gives a Boolean mask:
[True, False, True, False, False]
3. Use the mask to select or modify values
4. a[a > cutoff] = 0
This sets all values > 200 to 0.
Example
import numpy as np
a = [Link]([230, 10, 284, 39, 76])
cutoff = 200
a[a > cutoff] = 0
print(a)
Output
[0, 10, 0, 39, 76]
Why Masking Is Powerful
No need for loops
Very fast
Works on multidimensional arrays (images, matrices)
Code becomes shorter and cleaner
Broadcasting in NumPy
Broadcasting in NumPy is the automatic expansion of smaller arrays so that arithmetic
operations can be performed with larger arrays, following rules that allow stretching only
along dimensions of size 1.
OR
Broadcasting is NumPy’s rule that allows arrays of different shapes to be used together in
arithmetic operations.
NumPy automatically expands (stretches) smaller arrays so their shapes match, but only
along dimensions of size 1.
Why Broadcasting Is Useful
It lets you perform operations without writing loops, making computations faster and
cleaner.
Example:
a = [Link]([
[0, 1],
[2, 3],
[4, 5]
])
b = [Link]([10, 100])
a*b
Result
[[ 0 100]
[ 20 300]
[ 40 500]]
NumPy automatically stretched b to match the shape of a.
How Broadcasting Works (Rules Explain)
Rule 1: Compare shapes from right to left
Example:
a shape = (3, 2)
b shape = (2,) → treated as (1, 2)
Rule 2: Two dimensions are compatible if:
They are equal, or
One of them is 1, so it can stretch
Rule 3: Only dimensions of size 1 can stretch
Example That Fails
c = [Link]([
[0, 1, 2],
[3, 4, 5]
]) # shape (2, 3)
b = [Link]([10, 100]) # shape (2,)
c*b
Error because shapes (2, 3) and (2,) cannot match;
last dimensions 3 and 2 are not equal and neither is 1.
Fix Using None (adds new dimension)
c * b[:, None]
This makes b shape = (2, 1)
Now broadcasting works:
Result
[[ 0 10 20]
[300 400 500]]
dtype in NumPy
dtype in NumPy refers to the data type of array elements.
It defines the size, range, and behavior of numbers stored in the array.
Choosing the correct dtype prevents overflow and saves memory.
OR
dtype means data type of the elements stored in a NumPy array.
Examples of dtypes:
int8, int16, int32, int64
uint8 (unsigned int)
float32, float64
Each dtype has a fixed size in bits, which determines:
how large (or small) numbers it can store
how much memory it uses
how it behaves in calculations
Why dtype is important?
Because NumPy is designed for speed and memory efficiency.
Choosing the right dtype can:
save memory
avoid overflow errors
make computations faster
Example: int8 and uint8
int8
8-bit signed integer
Range: –128 to +127
uint8
8-bit unsigned integer
Range: 0 to 255
uint8 is used in images, because each pixel color (R, G, B) ranges from 0–255.
Ex:
dtype in NumPy array
import numpy as np
a = [Link]([200], dtype='uint8')
print(a)
Overflow Example (Very Important Concept)
a = [Link]([200], dtype='uint8')
a+a
Expected? → 400
Actual output → 144
Why?
Because:
max of uint8 = 255
200 + 200 = 400 (too big!)
NumPy wraps around using modulo 256
400 % 256 = 144
This is called overflow.
Fix Overflow (Use Bigger dtype)
a = [Link]([200], dtype='uint16')
a+a
Output:
array([400], dtype=uint16)
Works because uint16 can store up to 65535.
Why not always use huge dtypes?
Because:
1. More bits = more memory
2. Many data sources (like images) already come in uint8, so NumPy loads them as uint8
3. Using a huge dtype for everything is slow and unnecessary
Real-Life Example: Images
Each pixel in a color image = (R, G, B)
Each of R, G, B is a uint8 (0–255)
Example:
Black pixel: (0, 0, 0)
Red pixel: (255, 0, 0)
If you add two images:
You may expect brighter/dense colors
But because of uint8 overflow, you may get noise instead
Example:
255 + 255 = 510 → too big for uint8
510 % 256 = 254 → wrong color!
Changing dtype in NumPy (astype)
astype() in NumPy converts an array to a new dtype and returns a new array without
modifying the original.
If you have an existing NumPy array and you want to change its data type (dtype), you use
the method:
[Link](new_dtype)
This creates a new array with the new dtype.
(It does not modify the original array unless you assign it back.)
Example
import numpy as np
a = [Link]([200], dtype='uint8')
b = [Link]('uint64')
print(a) # Still uint8
print(b) # Converted to uint64
Output:
[200] # dtype=uint8
[200] # dtype=uint64
Counting Letters
You want to count how many times each letter appears in a string. This is called a frequency
table.
Dictionaries are perfect for this because they store data as key–value pairs, where:
Key → letter
Value → count of that letter
Example:
letter_counts = {}
for letter in "Mississippi":
letter_counts[letter] = letter_counts.get(letter, 0) + 1
letter_items = list(letter_counts.items())
letter_items.sort()
print(letter_items)
Output:
[('M', 1), ('i', 4), ('p', 2), ('s', 4)]
Files
A file is simply a named location on a storage device where data is kept permanently.
Examples:
"[Link]"
"[Link]"
"[Link]"
A Python program can:
✔ read from a file
✔ write to a file
✔ update a file
✔ create a new file
When a Python program is running, it stores all its data in RAM (Random Access Memory).
RAM is:
Fast
Cheap
Temporary (volatile) → data disappears when
program ends
computer shuts down
So, if we want to save information permanently, we cannot use RAM.
We must store data on non-volatile storage, such as:
Hard drive
USB drive
SSD
CD/DVD
These devices store data in files.
Files Are Like Notebooks
Working with a file is similar to using a notebook:
You must open it before reading or writing
You must close it when you are done
You can read from the beginning or skip around
You can write new pages or modify existing ones
Python uses the same idea.
Opening a File
To open a file, we specify:
1. Its name
2. The mode — what you want to do with it
Example:
file = open("[Link]", "r") # 'r' means read
Modes:
Mode Meaning
"r" read (file must exist)
"w" write (overwrites file)
"a" append (add to end)
"r+" read & write
Closing a File
After finishing reading or writing, close the file:
[Link]()
This ensures:
all data is saved
no corruption happens
system resources are freed
Writing Our First File
Program:
with open("[Link]", "w") as myfile:
[Link]("My first file written from Python\n")
[Link]("---------------------------------\n")
[Link]("Hello, world!\n")
Explanation:
Line 1: open("[Link]", "w")
o "w" means write mode
o If the file does not exist, it is created
o If it exists, it is overwritten
myfile is a file handle — like a TV remote control.
o You operate on the handle → changes happen to the real file.
Lines 2–4 use .write() to put text into the file.
The with block automatically closes the file at the end.
Output stored in [Link]
After running the code, the file contains:
My first file written from Python
---------------------------------
Hello, world!
Reading a File Line-by-Line
Program:
with open("[Link]", "r") as my_new_handle:
for the_line in my_new_handle:
print(the_line, end="")
Explanation:
"r" → read mode
The for loop reads each line in the file
print(..., end="") stops print from adding an extra newline
(because each line already contains \n)
Output on the screen:
My first file written from Python
---------------------------------
Hello, world!
What if the file does NOT exist?
Example:
mynewhandle = open("[Link]", "r")
Output:
FileNotFoundError: [Errno 2] No such file or directory: '[Link]'
This happens because read mode "r" cannot create files.
It can only read existing ones.
Turning a file into a list of lines:
Reading all the lines from a file and storing them in a Python list, where each element of the list
is one line from the file.
Definition:
It is the process of opening a file, reading every line in it, and saving those lines as separate
string elements inside a list.
Example:
If a file has:
Apple
Banana
Cherry
Then after turning it into a list of lines:
["Apple\n", "Banana\n", "Cherry\n"]
Each line becomes one item in the list
Newline characters (\n) are kept
Sometimes we have a text file and want to:
1. Read all lines
2. Store them in a list
3. Sort the list
4. Write the sorted lines to another file
Example:
A file named [Link] contains names and email addresses, one on each line.
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)
What readlines() Does
It reads the entire file at once.
Returns a list where:
o Each element is one line from the file.
o Newline characters \n are kept.
Example file:
Charlie
Alice
Bob
After .readlines():
["Charlie\n", "Alice\n", "Bob\n"]
After sorting:
["Alice\n", "Bob\n", "Charlie\n"]
Reading the Whole File at Once
Instead of reading a file line by line, we can read the entire file content into one single string.
This is useful when:
We don’t care about individual lines
We want to process the entire text together
We want to split it into words, characters, etc.
Example: Counting Words in a File
with open("[Link]") as f:
content = [Link]()
words = [Link]()
print("There are {0} words in the file.".format(len(words)))
Output:
Example 1
If [Link] contains:
Hello world
This is Python
Then:
Words: Hello, world, This, is, Python → 5 words
Output:
There are 5 words in the file.
Working with binary files
When we work with files in Python, we usually deal with text files—files that contain
readable characters (letters, numbers, punctuation).
However, not all files are text files. Many files, such as images, videos, audio files, PDFs,
and Word documents, contain binary data. These files cannot be read or written as plain
text.
Binary files are files that store data in raw bytes instead of characters.
A byte is a unit of digital information (8 bits)
Binary files store information in the same format the computer uses internally
Examples:
o Images: .jpg, .png
o Videos: .mp4
o Audio: .mp3
o Documents: .pdf, .docx
Unlike text files, binary files are not human-readable
Use of Binary Mode
In text mode ("r", "w"), Python tries to decode bytes into characters.
This works for text files but fails for binary files.
Binary mode ("rb", "wb", "ab") ensures that Python reads and writes the raw bytes
without any modification.
o No newline translation (\n ↔ \r\n)
o No character encoding/decoding
Key idea: Binary mode = raw, untouched data.
Binary File Modes
Mode Purpose
"rb" Read binary data
Write binary data (overwrite or
"wb"
create new)
"ab" Append binary data
Examples
Reading a Binary File
with open("[Link]", "rb") as f:
data = [Link]()
print("Number of bytes:", len(data))
Reads the entire file as bytes
Useful for analyzing or processing images, audio, etc.
Writing a Binary File
binary_data = b"\x41\x42\x43" # ASCII for A, B, C
with open("[Link]", "wb") as f:
[Link](binary_data)
b"" creates a byte sequence
Data is written exactly as raw bytes
Copying a Binary File
with open("[Link]", "rb") as f:
data = [Link]()
with open("copy_photo.jpg", "wb") as f:
[Link](data)
Reads the original file in binary mode
Writes it to a new file in binary mode
Produces an exact copy
Directories
Files on a computer are not stored randomly—they are organized using a file system.
A file system contains:
1. Files → Individual pieces of data (text files, images, etc.)
2. Directories (Folders) → Containers that hold files and/or other directories
This hierarchical structure allows for organized storage and easy access.
Current Directory
When you create a file, it is placed in the current directory (the folder you are “in”
when running your program).
When opening a file, Python looks in the current directory by default.
Example:
f = open("[Link]", "w") # creates [Link] in current directory
Specifying a Path
If the file is not in the current directory, you must provide its path:
Unix/Linux example:
wordsfile = open("/usr/share/dict/words", "r")
wordlist = [Link]()
print(wordlist[:6])
Output (first 6 lines):
['\n', 'A\n', "A's\n", 'AOL\n', "AOL's\n", 'Aachen\n']
Windows path example:
"C:/temp/[Link]"
"C:\\temp\\[Link]"
Backslashes \ need to be escaped (\\) in strings
Forward slashes / also work in Windows
Important Rules
1. / or \ cannot be part of a filename (reserved as directory separator)
2. Use absolute paths for files in other directories, or relative paths for files nearby
Fetching Data from the Web in Python
Python provides libraries that allow you to download data from the Internet.
This can be useful for:
Downloading files (text, images, PDFs)
Reading online resources directly into your program
Automating data collection or analysis
Using urllib (Standard Library)
The simplest way to fetch a web resource and save it to a local file is using
[Link]().
Example:
import [Link]
url = "[Link]
destination_filename = "[Link]"
[Link](url, destination_filename)
Explanation:
urlretrieve(url, filename) downloads the content from url
Saves it as filename in the current directory
If the file exists, it will overwrite it
Works for any type of file (text, image, etc.)
Using requests Module (Recommended)
The requests module is not in the standard library, but it is easier and more powerful than
urllib.
Example 1: Read entire web page into a string
import requests
url = "[Link]
response = [Link](url)
print([Link]) # prints the entire content as a string
[Link](url) returns a response object
[Link] contains the content of the page as a single string
Example 2: Read the response line by line
import requests
url = "[Link]
response = [Link](url)
for line in response.iter_lines():
print([Link]('utf-8')) # decode bytes to string
iter_lines() allows you to process the content line by line
Each line is a byte object, so .decode('utf-8') converts it to a string
Why Use requests Instead of urllib?
Cleaner syntax
More powerful options: headers, authentication, timeouts, sessions
Easier error handling
Directory: A named collection of files, also called a folder. Directories can contain files and
other directories, which are referred to as subdirectories of the directory that contains them.
File: A named entity, usually stored on a hard drive, floppy disk, or CD-ROM, that contains a
stream of characters.
File system: A method for naming, accessing, and organizing files and the data they contain.
Handle: An object in our program that is connected to an underlying resource (e.g. a file). The
file handle lets our program manipulate/read/write/close the actual file that is on our disk.
Path: A sequence of directory names that specifies the exact location of a file.