0% found this document useful (0 votes)
1 views67 pages

3 Module Notes

Uploaded by

msharsha148
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views67 pages

3 Module Notes

Uploaded by

msharsha148
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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.

You might also like