DATA SCIENCE
Unit Lecture Notes
Python for Data Science
Uses of Python • Environment Setup • Jupyter Notebook • Data Types
Tuples • Lists • Dictionaries • Operators • Functions
Topic Content Overview
1. Uses of Python Why Python, key libraries, Python vs R
2. Environment Setup Anaconda/pip, virtual environments, VS Code
3. Jupyter Notebook Cells, keyboard shortcuts, magic commands
4. Python Data Types int, float, str, bool, None — operations and pitfalls
5. Tuples Immutable sequences, indexing, unpacking, use cases
6. Lists Mutable sequences, methods, sorting, comprehensions
7. Dictionaries Key-value pairs, methods, nesting, comprehensions
8. Operators Arithmetic, comparison, logical, membership, identity
9. Functions def, parameters, return values, scope, lambda, docstrings
1. Uses of Python in Data Science
1.1 Why Python?
Three reasons Python dominates Data Science:
Readability: indentation-based blocks, minimal boilerplate, and clear syntax mean analysts can
be productive in weeks. A for loop in Python is one line; the same loop in Java is five.
Ecosystem: Python has the richest collection of data science, ML, and scientific computing
libraries of any language — NumPy, Pandas, Matplotlib, scikit-learn, TensorFlow, and PyTorch are
all Python-first.
Community: the largest data science community of any language. Almost any problem you
encounter has a Stack Overflow answer, a GitHub repository, or a tutorial in Python.
1.2 What Python is Used For
Domain What Python Does Key Libraries
Data Wrangling Cleaning, reshaping, filtering, merging pandas, NumPy
datasets
Data Visualisation Charts, plots, interactive dashboards Matplotlib, Seaborn, Plotly
Statistical Analysis Hypothesis testing, regression, SciPy, statsmodels
distributions
Machine Learning Classification, regression, clustering, scikit-learn
evaluation
Deep Learning Neural networks, CNNs, transformers TensorFlow, PyTorch, Keras
NLP Text cleaning, sentiment analysis, NLTK, spaCy, Transformers
language models
Web Scraping Extracting data from websites BeautifulSoup, requests
Automation Repetitive tasks, file processing os, shutil, pathlib
Databases Querying SQL and NoSQL SQLAlchemy, psycopg2,
pymongo
APIs Fetching data from REST APIs requests, httpx
Python vs R
R excels at statistical modelling and is preferred in academic statistics and bioinformatics.
Python is preferred in industry, ML engineering, and production systems. For this unit, Python is the primary
tool because of its versatility and direct path into ML and AI applications.
2. Environment Setup
2.1 Installing Python
Option A — Anaconda
What it includes: Python, conda package manager, 250+ pre-installed scientific packages, Jupyter
Notebook, Spyder IDE, and Anaconda Navigator GUI.
Download: [Link]
Best for: students who want everything working immediately, without manual package
installation.
Option B — Miniconda
What it includes: Python and conda only. Install only what you need.
Best for: users who want a lighter installation and will install specific packages as required.
Option C — Standard Python + pip
What it includes: Base Python only. Use pip to install libraries from PyPI.
Best for: users already comfortable with the command line.
2.2 Installing Packages with pip
pip is Python's standard package installer. It downloads packages from PyPI (the Python Package
Index at [Link]).
▶ Python: Installing packages with pip
# Install a single package
pip install numpy
# Install multiple packages at once
pip install pandas matplotlib seaborn scikit-learn
# Install a specific version
pip install pandas==2.1.0
# Upgrade an existing package
pip install --upgrade pandas
# Install from a requirements file
pip install -r [Link]
# List all installed packages
pip list
# Show details about one package
pip show pandas
2.3 Virtual Environments
A virtual environment is an isolated Python installation for a single project. It keeps project
dependencies separate, preventing version conflicts between projects.
Why they matter: Project A may need pandas 1.5 and Project B may need pandas 2.1. Without
virtual environments, only one version can be installed at a time. Virtual environments solve this by
giving each project its own isolated package space.
▶ Python: Creating and activating a virtual environment
# 1. Create a virtual environment named 'ds_env'
python -m venv ds_env
# 2. Activate it
# Windows:
ds_env\Scripts\activate
# macOS / Linux:
source ds_env/bin/activate
# 3. Your prompt now shows (ds_env) — all installs go here
(ds_env) pip install pandas numpy matplotlib jupyter
# 4. Save all package versions to a file
(ds_env) pip freeze > [Link]
# 5. Deactivate when done
deactivate
# Using conda instead:
conda create --name ds_env python=3.11
conda activate ds_env
conda install pandas numpy matplotlib
conda deactivate
2.4 VS Code Setup
Install VS Code: Download from [Link] — free and lightweight.
Python extension: Open Extensions (Ctrl+Shift+X) → search 'Python' → install Microsoft's Python
extension.
Select interpreter: Ctrl+Shift+P → 'Python: Select Interpreter' → choose your virtual
environment.
Jupyter extension: Search 'Jupyter' in Extensions → Install. Run .ipynb notebooks directly in VS
Code.
3. Jupyter Notebook Overview
3.1 What is Jupyter?
Jupyter Notebook is an interactive computing environment that combines live code, output, text,
equations, and visualisations in a single document (file extension: .ipynb). It is the standard working
environment for data scientists worldwide.
Why Jupyter suits Data Science: data science is exploratory — you try something, inspect the output,
adjust, and try again. Jupyter's cell-by-cell execution perfectly matches this workflow. You can run a
single cell to test a hypothesis without re-running the whole script.
3.2 Launching Jupyter
▶ Python: Starting Jupyter from the command line
# Activate your environment first, then:
jupyter notebook # classic interface, opens in browser
# OR — JupyterLab (modern interface, recommended)
jupyter lab
# Opens at [Link]
# Navigate to your project folder
# Click New → Python 3 to create a notebook
3.3 Cell Types
Cell Type Purpose How to Set
Code Write and run Python. Output appears directly Default — just type code
below the cell.
Markdown Formatted text: headings, bold, tables, LaTeX Esc → M or Cell menu → Markdown
equations. Does NOT execute Python.
Raw Plain text, not executed or rendered. Rarely Esc → R
used.
3.4 Essential Keyboard Shortcuts
Shortcut Action Mode
Shift + Enter Run cell and advance to next cell Both
Ctrl + Enter Run cell and stay in same cell Both
Alt + Enter Run cell and insert new cell below Both
Esc Enter Command mode (cell border turns Edit → Command
blue)
Enter Enter Edit mode (cursor inside cell) Command → Edit
A (command) Insert new cell Above current cell Command
B (command) Insert new cell Below current cell Command
D D (command) Delete current cell (press D twice) Command
Z (command) Undo cell deletion Command
M (command) Convert cell to Markdown Command
Y (command) Convert cell to Code Command
Ctrl + S Save notebook Both
Ctrl + Shift + - Split cell at cursor position Edit
3.5 Magic Commands
Magic commands are special Jupyter commands starting with % (line magic) or %% (cell magic). They
control Jupyter's behaviour rather than executing Python code.
▶ Python: Jupyter magic commands
# Time a single line (one run)
%time x = [i**2 for i in range(100000)]
# Time average over many runs (more accurate)
%timeit x = [i**2 for i in range(100000)]
# 10.2 ms ± 42.3 µs per loop (mean ± std. dev. of 7 runs)
# List all variables in the namespace
%whos
# Run an external Python script
%run my_script.py
# Show matplotlib plots inline
%matplotlib inline
# %%time — time the ENTIRE cell
%%time
total = 0
for i in range(1_000_000):
total += i
# Show all outputs from a cell (not just the last line)
from [Link] import InteractiveShell
InteractiveShell.ast_node_interactivity = 'all'
Best Practices for Notebooks
One logical task per cell: keep cells focused for easier debugging.
Restart and Run All regularly: Kernel → Restart & Run All confirms cells work in order. Cells run out
of order create hidden bugs.
Document with Markdown: explain your analysis between code cells. A notebook should tell a story,
not just run code.
4. Python Data Types
4.1 Overview
Every value in Python has a type. Python is dynamically typed — you do not declare types. Python
infers the type from the value assigned. The five core built-in types are: int, float, str, bool, and
NoneType.
▶ Python: Checking data types with type()
x = 42; print(type(x)) # <class 'int'>
y = 3.14; print(type(y)) # <class 'float'>
s = 'hello'; print(type(s)) # <class 'str'>
b = True; print(type(b)) # <class 'bool'>
n = None; print(type(n)) # <class 'NoneType'>
4.2 Integers (int)
Integers are whole numbers — positive, negative, or zero. In Python 3, integers have unlimited
precision (no maximum size).
▶ Python: Integer operations
a, b = 17, 5
print(a + b) # 22 — addition
print(a - b) # 12 — subtraction
print(a * b) # 85 — multiplication
print(a ** b) # 1419857 — exponentiation (17^5)
print(a // b) # 3 — floor (integer) division
print(a % b) # 2 — modulo (remainder)
# Conversion to int
int(3.9) # 3 — truncates (does NOT round)
int('42') # 42 — string to int
int(True) # 1 — bool to int
# Very large integers work fine
googol = 10 ** 100
print(googol) # 100-digit number
4.3 Floats (float)
Floats represent numbers with a decimal point. Python uses 64-bit double-precision (IEEE 754), giving
approximately 15–17 significant decimal digits.
▶ Python: Float operations and the precision trap
pi = 3.14159; e = 2.71828
print(pi + e) # 5.85987
print(pi * 2) # 6.28318
# Scientific notation
avogadro = 6.022e23 # 6.022 × 10²³
electron = 9.109e-31 # mass of electron in kg
# ⚠ FLOATING-POINT PRECISION TRAP
print(0.1 + 0.2) # 0.30000000000000004 (NOT 0.3!)
print(0.1 + 0.2 == 0.3) # False
# Solution: use round() for display, or the decimal module for money
print(round(0.1 + 0.2, 2)) # 0.3
⚠ Floating-Point Warning
Never use == to compare floats. Due to binary representation limits, 0.1 + 0.2 is not exactly 0.3. Instead,
check if the difference is smaller than a tiny threshold: abs(a - b) < 1e-9. This is critical in data
science when comparing computed results.
4.4 Strings (str)
Strings are sequences of characters enclosed in single, double, or triple quotes. Strings are
immutable — you cannot change a character in place; you create a new string instead.
▶ Python: String operations and methods
# Creation
s1 = 'Hello'; s2 = "World"
s3 = '''Multi-line
string here'''
# Concatenation and repetition
print(s1 + ' ' + s2) # 'Hello World'
print('Ha' * 3) # 'HaHaHa'
print(len(s1)) # 5
# Indexing (zero-based) and slicing
s = 'Python'
print(s[0]) # 'P' — first character
print(s[-1]) # 'n' — last character
print(s[0:3]) # 'Pyt' — slice [start:stop]
print(s[::-1]) # 'nohtyP' — reversed
# Key methods
text = ' Hello, Data Science! '
print([Link]()) # remove whitespace
print([Link]()) # lowercase
print([Link]()) # uppercase
print([Link]('Hello','Hi'))# replace substring
print([Link](',')) # split → list
# f-strings (Python 3.6+) — most readable way to format
name = 'Alice'; score = 95.7
print(f'Student: {name}, Score: {score:.1f}%')
# Student: Alice, Score: 95.7%
# Membership testing
print('Data' in 'Data Science') # True
4.5 Booleans (bool)
Booleans represent True or False. They are a subtype of integers: True == 1 and False == 0. Most
Python objects can be evaluated as booleans, which is used extensively in conditionals.
▶ Python: Boolean operations and truthiness
# Logical operators
print(True and False) # False
print(True or False) # True
print(not True) # False
# FALSY values — evaluate to False in a boolean context:
bool(0) # False
bool('') # False — empty string
bool([]) # False — empty list
bool(None) # False
# TRUTHY values — everything else:
bool(1) # True
bool('hi') # True
bool([1,2]) # True
# Practical filtering: remove zeros from a list
scores = [85, 0, 92, 0, 78]
non_zero = [s for s in scores if s] # [85, 92, 78]
4.6 None
None is Python's null value — it represents the absence of a value. It is its own type (NoneType) and
there is only one None object in Python. Functions that return nothing implicitly return None.
▶ Python: Working with None
x = None
print(type(x)) # <class 'NoneType'>
# Always use 'is None' to check — NOT '== None'
if x is None:
print('x has no value assigned')
# In Data Science, None represents missing data.
# pandas uses NaN for missing numeric data and None for missing objects.
student_grade = None # grade not yet submitted
5. Tuples
5.1 What is a Tuple?
A tuple is an ordered, immutable sequence of elements. Once created, a tuple cannot be modified
— you cannot add, remove, or change elements. Tuples are defined with parentheses () or just
commas.
▶ Python: Creating tuples
# Empty tuple
t0 = ()
# Single element — the comma is ESSENTIAL
t1 = (42,) # This IS a tuple
t2 = (42) # This is just the integer 42, NOT a tuple!
# Multi-element tuples
coords = (10.5, 20.3) # latitude, longitude
person = ('Alice', 30, 'F') # name, age, gender
rgb = (255, 128, 0) # red-green-blue colour
mixed = (1, 'hello', True)
# Tuple packing — parentheses are optional
point = 3, 4 # same as (3, 4)
# Convert other sequences to tuple
from_list = tuple([1, 2, 3]) # (1, 2, 3)
from_str = tuple('abc') # ('a', 'b', 'c')
print(type(coords)) # <class 'tuple'>
5.2 Indexing and Slicing
▶ Python: Accessing tuple elements
t = (10, 20, 30, 40, 50)
# Positive indexing (0-based)
print(t[0]) # 10 — first element
print(t[2]) # 30 — third element
print(t[-1]) # 50 — last element
print(t[-2]) # 40 — second from last
# Slicing [start:stop:step] — stop is EXCLUSIVE
print(t[1:4]) # (20, 30, 40)
print(t[:3]) # (10, 20, 30) — from beginning
print(t[2:]) # (30, 40, 50) — to end
print(t[::2]) # (10, 30, 50) — every 2nd element
print(t[::-1]) # (50, 40, 30, 20, 10) — reversed
# Nested tuple access
matrix = ((1, 2), (3, 4), (5, 6))
print(matrix[1]) # (3, 4)
print(matrix[1][0]) # 3
5.3 Tuple Unpacking
Tuple unpacking assigns tuple elements to individual variables in one line — one of Python's most
elegant features.
▶ Python: Tuple unpacking
# Basic unpacking
coords = (48.8, 2.35) # Paris: lat, lon
lat, lon = coords
print(lat) # 48.8
print(lon) # 2.35
# Swap two variables — no temporary variable needed
a, b = 10, 20
a, b = b, a
print(a, b) # 20 10
# Extended unpacking with *
first, *rest = (1, 2, 3, 4, 5)
print(first) # 1
print(rest) # [2, 3, 4, 5]
*start, last = (1, 2, 3, 4, 5)
print(last) # 5
# Unpacking in a for loop — very common pattern
students = [('Alice', 90), ('Bob', 85), ('Carol', 92)]
for name, score in students:
print(f'{name}: {score}')
# Alice: 90 Bob: 85 Carol: 92
5.4 Tuple Methods and Operations
▶ Python: Tuple methods
t = (1, 2, 3, 2, 4, 2, 5)
# count() — how many times a value appears
print([Link](2)) # 3
# index() — position of first occurrence
print([Link](3)) # 2
print([Link](2)) # 1 (first occurrence only)
# Membership testing
print(3 in t) # True
print(9 in t) # False
# Concatenation and repetition (creates new tuple)
print((1, 2) + (3, 4)) # (1, 2, 3, 4)
print((1, 2) * 3) # (1, 2, 1, 2, 1, 2)
# Trying to modify a tuple raises TypeError:
# t[0] = 99 → TypeError: 'tuple' object does not support item assignment
5.5 When to Use Tuples vs Lists
Criterion Tuple List
Mutability Immutable — cannot be changed Mutable — can be changed
Syntax Parentheses () or just commas Square brackets []
Performance Slightly faster, less memory Slightly slower, more memory
Can be dict key Yes (hashable) No (not hashable)
Typical use Fixed data: coordinates, DB records, Variable collections that grow or shrink
RGB values
6. Lists
6.1 What is a List?
A list is an ordered, mutable sequence of elements. It is the most versatile and commonly used data
structure in Python. Lists are defined with square brackets [].
▶ Python: Creating lists
# Empty list
empty = []
# Homogeneous lists
scores = [85, 92, 78, 95, 88]
names = ['Alice', 'Bob', 'Carol', 'Dave']
prices = [19.99, 34.50, 5.99]
# Mixed-type (Python allows this)
record = ['Alice', 30, True, 95.5, None]
# Nested list (2D matrix)
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
# Create from other sequences
from_range = list(range(5)) # [0, 1, 2, 3, 4]
from_tuple = list((1, 2, 3)) # [1, 2, 3]
from_string = list('hello') # ['h','e','l','l','o']
6.2 Indexing, Slicing, and Mutability
▶ Python: Indexing, slicing, and modifying lists
fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
# Indexing
print(fruits[0]) # 'apple'
print(fruits[-1]) # 'elderberry'
# Slicing (returns a new list)
print(fruits[1:3]) # ['banana', 'cherry']
print(fruits[:2]) # ['apple', 'banana']
print(fruits[::2]) # ['apple', 'cherry', 'elderberry']
print(fruits[::-1]) # reversed list
# MUTABILITY — unlike tuples, you CAN change elements
fruits[0] = 'avocado'
print(fruits) # ['avocado', 'banana', 'cherry', 'date', 'elderberry']
# Accessing nested list elements
matrix = [[1,2,3],[4,5,6],[7,8,9]]
print(matrix[1][2]) # 6 (row 1, column 2)
6.3 List Methods — Modifying
▶ Python: Adding and removing elements
nums = [3, 1, 4, 1, 5, 9]
# append() — add ONE element to the end
[Link](2)
print(nums) # [3, 1, 4, 1, 5, 9, 2]
# insert(index, value) — insert at a specific position
[Link](0, 0) # insert 0 at index 0
# extend() — add all elements from another iterable
[Link]([6, 5])
# equivalent to: nums += [6, 5]
# remove() — remove FIRST occurrence of a value
[Link](1) # removes the first 1
# pop(index) — remove AND return element at index
last = [Link]() # removes last element
third = [Link](2) # removes element at index 2
# del — delete by index or slice
del nums[0]
del nums[1:3]
# clear() — remove all elements
[Link]() # nums is now []
6.4 List Methods — Searching and Sorting
▶ Python: Searching and sorting lists
fruits = ['banana', 'apple', 'cherry', 'apple', 'date']
# index() — first position of a value
print([Link]('apple')) # 1
# count() — how many times a value appears
print([Link]('apple')) # 2
# sort() — sort IN PLACE (modifies the list)
[Link]()
print(fruits) # ['apple', 'apple', 'banana', 'cherry', 'date']
[Link](reverse=True) # descending
# sorted() — returns a NEW sorted list, original unchanged
nums = [5, 2, 8, 1, 9, 3]
sorted_nums = sorted(nums) # [1, 2, 3, 5, 8, 9]
print(nums) # [5, 2, 8, 1, 9, 3] — unchanged
# Sort by a custom key
students = [('Bob', 85), ('Alice', 92), ('Carol', 78)]
[Link](key=lambda s: s[1], reverse=True)
# [('Alice', 92), ('Bob', 85), ('Carol', 78)]
# Other useful operations
print(min(nums), max(nums), sum(nums)) # 1 9 28
print(len(nums)) # 6
6.5 List Comprehensions
List comprehensions are a concise, Pythonic way to build new lists. They are faster than equivalent
for loops and are central to idiomatic Python.
Syntax: [ expression for item in iterable if condition ]
▶ Python: List comprehensions — from basic to advanced
# Squares of 0–9
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Equivalent for loop (slower, more verbose):
squares = []
for x in range(10):
[Link](x**2)
# With condition: only even squares
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# Transform strings
names = ['alice', 'bob', 'carol']
upper = [[Link]() for name in names]
# ['ALICE', 'BOB', 'CAROL']
# Filter a list
scores = [85, 42, 91, 67, 38, 95]
passing = [s for s in scores if s >= 50]
# [85, 91, 67, 95]
# Conditional expression (ternary) inside comprehension
grades = ['Pass' if s >= 50 else 'Fail' for s in scores]
# ['Pass', 'Fail', 'Pass', 'Pass', 'Fail', 'Pass']
# Flatten a 2D matrix into a 1D list
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat = [val for row in matrix for val in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
7. Dictionaries
7.1 What is a Dictionary?
A dictionary is an unordered collection of key-value pairs. Each key maps to exactly one value. Keys
must be unique and immutable (strings, numbers, or tuples). Values can be anything. Ordered by
insertion since Python 3.7.
▶ Python: Creating dictionaries
# Empty dict
d = {} # or: d = dict()
# Dictionary literal
student = {'name': 'Alice', 'age': 22, 'gpa': 3.8}
# Mixed value types
record = {
'id': 12345,
'name': 'Alice Doe',
'scores': [85, 92, 78], # list as value
'active': True,
'email': None
}
# From keyword arguments
d2 = dict(name='Bob', age=25, city='Kigali')
# From a list of (key, value) pairs
pairs = [('a', 1), ('b', 2), ('c', 3)]
d3 = dict(pairs) # {'a': 1, 'b': 2, 'c': 3}
7.2 Accessing and Modifying Values
▶ Python: CRUD operations on dictionaries
student = {'name': 'Alice', 'age': 22, 'gpa': 3.8}
# READ — access by key
print(student['name']) # 'Alice'
# KeyError if key doesn't exist:
# student['email'] → KeyError: 'email'
# Safe access with get() — returns None (or a default) if missing
print([Link]('email')) # None
print([Link]('email', 'N/A')) # 'N/A'
# CREATE / UPDATE — add or change a key
student['email'] = 'alice@[Link]' # add new key
student['age'] = 23 # update existing key
# Update multiple keys at once
[Link]({'age': 24, 'gpa': 3.9})
# DELETE
del student['email']
gpa = [Link]('gpa') # removes and returns the value
# Check key existence
print('name' in student) # True
print('gpa' in student) # False (we just removed it)
7.3 Iterating over Dictionaries
▶ Python: Looping over keys, values, and items
grades = {'Alice': 90, 'Bob': 85, 'Carol': 92, 'Dave': 78}
# Iterate over KEYS (default)
for name in grades: # same as: for name in [Link]()
print(name)
# Iterate over VALUES
for score in [Link]():
print(score)
average = sum([Link]()) / len(grades) # 86.25
# Iterate over KEY-VALUE PAIRS — most common
for name, score in [Link]():
print(f'{name}: {score}')
# Convert to lists
print(list([Link]())) # ['Alice', 'Bob', 'Carol', 'Dave']
print(list([Link]())) # [90, 85, 92, 78]
# setdefault() — get value, or set default if key missing
[Link]('Eve', 0) # adds Eve:0 only if Eve not present
# Merge two dicts (Python 3.9+)
extra = {'Frank': 88}
merged = grades | extra
7.4 Nested Dictionaries
Dictionaries can contain other dictionaries, creating hierarchical data — the same structure as JSON
(the standard format for API data).
▶ Python: Nested dictionaries — student database example
students = {
'STU001': {
'name': 'Alice Doe',
'school': 'SBITE',
'grades': {'Data Science': 88, 'ML': 92},
'active': True
},
'STU002': {
'name': 'Bob Smith',
'school': 'SNHS',
'grades': {'Biology': 79, 'Chemistry': 83},
'active': False
}
}
# Access nested values
print(students['STU001']['name']) # 'Alice Doe'
print(students['STU001']['grades']['ML']) # 92
# Safe nested access
ml = [Link]('STU001',{}).get('grades',{}).get('ML','N/A')
# Iterate over all students
for sid, info in [Link]():
print(f"{sid}: {info['name']} — Active: {info['active']}")
7.5 Dictionary Comprehensions
Syntax: { key_expr : value_expr for item in iterable if condition
}
▶ Python: Dictionary comprehensions
# Map each number to its square
squares = {x: x**2 for x in range(6)}
# {0:0, 1:1, 2:4, 3:9, 4:16, 5:25}
# Invert a dictionary (swap keys and values)
original = {'a': 1, 'b': 2, 'c': 3}
inverted = {v: k for k, v in [Link]()}
# {1: 'a', 2: 'b', 3: 'c'}
# Filter: keep only passing students
grades = {'Alice': 90, 'Bob': 45, 'Carol': 82, 'Dave': 35}
passing = {name: s for name, s in [Link]() if s >= 50}
# {'Alice': 90, 'Carol': 82}
# Normalise scores to 0–1 scale
top = max([Link]()) # 90
normalised = {n: round(s/top, 3) for n, s in [Link]()}
# {'Alice': 1.0, 'Bob': 0.5, 'Carol': 0.911, 'Dave': 0.389}
8. Operators
8.1 Arithmetic Operators
Operator Operation Example Result
+ Addition 7+3 10
- Subtraction 7-3 4
* Multiplication 7*3 21
/ Division (always float) 7/2 3.5
// Floor division 7 // 2 3
% Modulo (remainder) 7%3 1
** Exponentiation 2 ** 10 1024
▶ Python: Arithmetic operators and augmented assignment
a, b = 17, 5
print(a / b) # 3.4 — always returns float
print(a // b) # 3 — floor division (rounds down)
print(a % b) # 2 — remainder (useful for even/odd check)
print(a ** b) # 1419857 — 17 to the power 5
# Check if n is even
n = 42
print(n % 2 == 0) # True
# Augmented assignment operators
x = 10
x += 5 # x = 15 (same as x = x + 5)
x -= 3 # x = 12
x *= 2 # x = 24
x //= 5 # x = 4
x **= 3 # x = 64
8.2 Comparison Operators
Comparison operators compare two values and return a Boolean (True or False).
Operator Meaning Example Result
== Equal to 5 == 5 True
!= Not equal to 5 != 3 True
> Greater than 7>3 True
< Less than 3<7 True
>= Greater than or equal 5 >= 5 True
<= Less than or equal 3 <= 7 True
▶ Python: Comparison operators and chaining
print(10 > 5) # True
print(3 == 3.0) # True — int and float compare equal
print('abc' == 'ABC') # False — case sensitive
# Python allows CHAINED comparisons — very readable
age = 25
print(18 <= age <= 65) # True — working age bracket
print(0 < 7 < 10) # True
# Comparing strings — lexicographic (alphabetical) order
print('apple' < 'banana') # True
# Comparing lists — element by element
print([1, 2, 3] < [1, 2, 4]) # True (3 < 4 at index 2)
8.3 Logical Operators
Operator Meaning Example Result
and Both must be True True and False False
or At least one True False or True True
not Inverts Boolean not True False
▶ Python: Logical operators in practice
age = 22
income = 50000
has_id = True
# Compound condition
eligible = age >= 18 and income > 30000 and has_id
print(eligible) # True
# Short-circuit evaluation: 'and' stops at first False
def risky(): raise Exception('called!')
print(False and risky()) # False — risky() never called
# 'or' stops at first True
print(True or risky()) # True — risky() never called
# Practical: provide a default value
name = '' or 'Anonymous' # '' is falsy → uses 'Anonymous'
print(name) # 'Anonymous'
# Operator precedence: not > and > or
print(True or False and False) # True (and evaluated first)
print((True or False) and False) # False (brackets change order)
8.4 Membership and Identity Operators
▶ Python: in, not in, is, is not
# Membership operators — test if value is in a sequence
fruits = ['apple', 'banana', 'cherry']
print('apple' in fruits) # True
print('mango' not in fruits) # True
# Works on strings
print('ello' in 'Hello') # True
# Works on dicts — checks KEYS (not values)
d = {'a': 1, 'b': 2}
print('a' in d) # True
print(1 in d) # False — 1 is a value, not a key
print(1 in [Link]()) # True
# Identity operators — test if same OBJECT in memory
# Use 'is' ONLY for None, True, False
x = None
print(x is None) # True — correct way to check for None
# WARNING: 'is' vs '=='
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — same VALUE
print(a is b) # False — different objects in memory
print(a is c) # True — c points to the SAME object as a
9. Functions
9.1 Defining and Calling Functions
A function is a named, reusable block of code. Functions are defined with the def keyword. The
DRY principle — Don't Repeat Yourself — says: if you write the same code more than twice, put it in a
function.
▶ Python: Basic function definition and calling
# Function with no parameters
def greet():
print('Hello, Data Science!')
greet() # call the function → Hello, Data Science!
# Function with parameters
def greet_person(name):
print(f'Hello, {name}!')
greet_person('Alice') # Hello, Alice!
greet_person('Bob') # Hello, Bob!
# Function with return value
def square(n):
return n ** 2
result = square(7)
print(result) # 49
# Function with multiple parameters
def calculate_bmi(weight_kg, height_m):
bmi = weight_kg / (height_m ** 2)
return round(bmi, 1)
print(calculate_bmi(70, 1.75)) # 22.9
9.2 Parameters and Arguments
▶ Python: Default parameters, keyword arguments, *args, **kwargs
# Default parameter values
def power(base, exponent=2): # exponent defaults to 2
return base ** exponent
print(power(3)) # 9 — uses default exponent=2
print(power(3, 3)) # 27 — overrides default
print(power(2, 10)) # 1024
# Keyword arguments — pass by name, any order
def describe(name, age, school):
return f'{name}, age {age}, at {school}'
describe(age=22, school='SBITE', name='Alice') # order irrelevant
# *args — accept ANY number of positional arguments
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100
# **kwargs — accept ANY number of keyword arguments
def show_info(**details):
for key, value in [Link]():
print(f' {key}: {value}')
show_info(name='Alice', score=92, city='Kigali')
# name: Alice
# score: 92
# city: Kigali
9.3 Return Values
▶ Python: Single and multiple return values
# Functions without return → return None automatically
def print_hello():
print('Hello')
result = print_hello() # prints Hello
print(result) # None
# Return multiple values — returned as a tuple
def min_max(numbers):
return min(numbers), max(numbers)
lo, hi = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f'Min: {lo}, Max: {hi}') # Min: 1, Max: 9
# Early return — exit the function before reaching the end
def safe_divide(a, b):
if b == 0:
return None # early return avoids ZeroDivisionError
return a / b
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None
9.4 Variable Scope — LEGB Rule
Python uses the LEGB rule: Local → Enclosing → Global → Built-in. Variables created inside a
function are local to that function and cannot be accessed from outside.
▶ Python: Scope demonstration — LEGB rule
x = 'global'
def outer():
x = 'enclosing'
def inner():
x = 'local'
print(x) # 'local' — uses innermost x
inner()
print(x) # 'enclosing'
outer()
print(x) # 'global'
# global keyword — modify a global variable inside a function
count = 0
def increment():
global count
count += 1
increment(); increment()
print(count) # 2
# Best practice: avoid global variables.
# Pass values as arguments and return results instead.
9.5 Lambda Functions
Lambda functions are small, anonymous functions defined with the lambda keyword. They can
have any number of arguments but only ONE expression. Most useful as an argument to sorted(),
map(), or filter().
Syntax: lambda parameters : expression
▶ Python: Lambda functions
# Equivalent normal and lambda function
def square(x): return x**2
square_l = lambda x: x**2
print(square(5)) # 25
print(square_l(5)) # 25
# Lambda with multiple parameters
add = lambda a, b: a + b
print(add(3, 4)) # 7
# Most common use: key in sorted()
students = [('Alice', 92), ('Bob', 85), ('Carol', 78), ('Dave', 95)]
# Sort by score descending
by_score = sorted(students, key=lambda s: s[1], reverse=True)
# [('Dave', 95), ('Alice', 92), ('Bob', 85), ('Carol', 78)]
# With map() — apply function to every element
scores = [85, 92, 78, 95]
scaled = list(map(lambda s: round(s * 0.9, 1), scores))
# [76.5, 82.8, 70.2, 85.5]
# With filter() — keep elements where function returns True
passing = list(filter(lambda s: s >= 80, scores))
# [85, 92, 95]
9.6 Docstrings
Docstrings document a function. Placed immediately after the def line in triple quotes, they describe
what the function does, its parameters, and return value. Good docstrings are a professional habit
and enable the help() system.
▶ Python: Function with a complete docstring
def calculate_stats(numbers):
'''
Calculate basic descriptive statistics for a list of numbers.
Parameters
----------
numbers : list of int or float
Returns
-------
dict with keys: count, mean, median, min, max
Example
-------
>>> calculate_stats([1, 2, 3, 4, 5])
{'count': 5, 'mean': 3.0, 'median': 3, 'min': 1, 'max': 5}
'''
if not numbers:
return None
n = len(numbers)
srt = sorted(numbers)
mid = n // 2
med = srt[mid] if n % 2 else (srt[mid-1] + srt[mid]) / 2
return {'count': n, 'mean': sum(numbers)/n,
'median': med, 'min': min(numbers), 'max': max(numbers)}
# Access the docstring
help(calculate_stats)
# Test it
print(calculate_stats([85, 92, 78, 95, 88]))
# {'count': 5, 'mean': 87.6, 'median': 88, 'min': 78, 'max': 95}
Summary and Quick Reference
Data Structure Comparison
Structure Syntax Ordered Mutable Duplicate Keys Best For
Tuple (1, 2, 3) Yes No ✗ N/A Fixed records, coordinates,
dict keys
List [1, 2, 3] Yes Yes ✓ Yes Any ordered, changeable
collection
Dictionary {'a': 1} Yes (3.7+) Yes ✓ No (keys Key-value lookup, JSON-like
unique) data
Operator Quick Reference
Category Operators Example
Arithmetic + − * / // % ** 7//2 → 3, 7%3 → 1, 2**8 → 256
Comparison == != > < >= <= 5 >= 5 → True, 'a' < 'b' → True
Logical and or not age>=18 and has_id → True
Membership in not in 'x' in 'text' → True
Identity is is not x is None → True/False
Augmented += −= *= /= **= //= x += 1 is shorthand for x = x + 1
Python Best Practices for Data Science
Use descriptive variable names: df_sales not d; total_revenue not tr.
Write docstrings: every function should document inputs, outputs, and an example.
Use list/dict comprehensions: faster and more Pythonic than equivalent for loops.
Prefer tuples for fixed data: signals to readers that the data will not change.
Never compare floats with ==: use abs(a-b) < 1e-9 instead.
Use .get() on dicts: avoids KeyError when a key may be absent.
Restart and Run All in Jupyter: always test your notebook in order before submitting.
Use virtual environments: one per project to prevent dependency conflicts.
Data Science — Python Foundations Lecture Notes | 2025–2026
Uses of Python • Environment Setup • Jupyter • Data Types • Tuples • Lists • Dictionaries • Operators •
Functions