Using the random and datetime modules to model uncertainty, work with calendar data, and run programs that observe what usually happens over many trials
Every program you have written so far has produced the same output every time it runs — given the same inputs, the same results appear. But many useful programs need to work differently. A quiz program should shuffle its questions. A game should generate unpredictable outcomes. A scheduling tool should know today's date. A scientific model should run thousands of slightly different trials and report what usually happens.
This chapter introduces two standard-library tools — random and datetime — along with the idea of simulation: using a program to model a situation, run it many times, and observe the results. These tools appear throughout computing, from games and testing to scientific research and probability modeling.
By the end of this chapter you will be able to generate random numbers and make random choices, work with dates and times, build simple simulations, run repeated trials, and interpret what simulation results mean.
Chapter 15 gave you a first look at import random and import math. Chapter 21 used import csv. This chapter goes deeper into random and introduces datetime. The loops, functions, lists, and dictionaries from earlier parts of the book all reappear here — randomness and dates become most powerful when you combine them with the structures you already know.
By the end of this chapter, you should be able to:
random.randint(), random.random(), and random.uniform() to generate random numbers in a specified range.random.choice(), random.shuffle(), and random.sample() to make random selections from sequences.random.seed() to make random sequences reproducible for testing.datetime.date to represent and compare calendar dates.strftime() and parse date strings using strptime().datetime.timedelta.random and datetime; how they relate to the standard library pattern already established in Chapter 15; simulation as technique rather than modulerandint, random(), uniform(); tracing programs that use random values; testing with representative cases at boundary conditionsrandom.choice(), random.shuffle(), random.sample(); common mistake with shuffle returning None; comparison table of all three functionsrandom.seed() syntaxdatetime.date; date.today(); constructing specific dates; reading .year, .month, .daystrftime() format codes; strptime() for parsing date strings; combining codes with separatorstimedelta; subtracting two dates; adding or subtracting duration from a date; timing code with datetime.datetime.now()You have already used the standard library throughout this course. Chapter 15 introduced math and gave you a first look at random. Chapter 21 used the csv module. Chapter 23 used exception types like ValueError and FileNotFoundError. The standard library is a large collection of modules included with every Python installation — you do not need to install anything extra to use them.
This chapter focuses on two modules that are especially useful for practical programs:
random — for generating unpredictable values, making selections, and shuffling sequencesdatetime — for working with dates, times, and durationsBoth modules follow the same import pattern you already know.
import random
import datetime
After importing, you call their functions using dot notation: random.randint(...), datetime.date.today(), and so on. This chapter also introduces simulation, which is not a module but a programming technique. A simulation uses a program to model a real-world situation — often one involving randomness — and runs it many times to observe patterns.
The random module generates what are technically called pseudo-random numbers — numbers that pass statistical tests for randomness but are actually produced by a deterministic algorithm. For the purposes of this course, they behave as random numbers. For cryptographic applications, Python provides the secrets module, which uses a different approach.
Random numbers are numbers chosen without a predictable pattern. Python's random module provides several functions for generating them.
Returns a random integer between a and b, inclusive. Both endpoints can be chosen.
import random
roll = random.randint(1, 6)
print("You rolled:", roll)
Every time this program runs, roll is one of: 1, 2, 3, 4, 5, or 6. All six values are equally likely.
Returns a random float between 0.0 and 1.0, not including 1.0. This is useful when you want a probability value or a percentage.
import random
chance = random.random()
print(chance) # something like 0.4732891...
Returns a random float between a and b. Use this when you need a decimal value in a specific range.
import random
temperature = random.uniform(36.1, 37.2)
print(f"Simulated temperature: {temperature:.1f} degrees")
Because random values change every run, tracing a program that uses randomness works differently from tracing one with fixed values. When you trace, choose a representative example value and trace with that — then trace again with a different value near a boundary.
score = random.randint(0, 100)| Line | Variable | Value (Trace A) | Value (Trace B) | Output |
|---|---|---|---|---|
| 3 | score | 73 | 45 | |
| 5 | score ≥ 60 | True | False | |
| 6 / 8 | branch | if-branch | else-branch | Pass / Fail |
Sometimes you do not want a random number — you want a random item from a list. The random module provides three functions for this, and understanding the differences between them matters.
Returns one randomly selected item from a sequence. The sequence must not be empty.
import random
colors = ["red", "green", "blue", "yellow"]
picked = random.choice(colors)
print("Chosen color:", picked)
Randomly rearranges the items of a list in place. The list itself is modified. Nothing is returned.
import random
questions = ["Q1", "Q2", "Q3", "Q4", "Q5"]
random.shuffle(questions)
print(questions) # same five items, unpredictable order
random.shuffle() modifies the list directly and returns None. A very common error is assigning the result back to the variable: questions = random.shuffle(questions). This sets questions to None and loses your list. Call random.shuffle(questions) on its own line, without assignment.
Returns a new list of k items chosen from sequence without repetition. The original sequence is unchanged.
import random
students = ["Ana", "Ben", "Cara", "Diego", "Eve", "Finn"]
selected = random.sample(students, 3)
print("Selected:", selected) # 3 unique names, order varies
NoneOne of the most powerful uses of randomness in programming is running a process many times and observing what usually happens. This is called a simulation. The basic structure of any simulation is:
import random
trials = 1000
count_of_interest = 0
for i in range(trials):
# Simulate one trial
outcome = ... # some random result
if outcome # meets some condition:
count_of_interest += 1
proportion = count_of_interest / trials
print(f"Proportion: {proportion:.3f}")
flip_coin() function that returns "heads" or "tails" with equal probability. Run 10,000 trials. Count heads and compute the proportion.import random
def flip_coin():
return random.choice(["heads", "tails"])
trials = 10000
heads_count = 0
for i in range(trials):
result = flip_coin()
if result == "heads":
heads_count += 1
proportion = heads_count / trials
print(f"Heads in {trials} flips: {heads_count}")
print(f"Proportion: {proportion:.3f}")
Heads in 10000 flips: 4987
Proportion: 0.499
import random
def roll_two_dice():
die1 = random.randint(1, 6)
die2 = random.randint(1, 6)
return die1 + die2
trials = 100000
sevens = 0
for i in range(trials):
total = roll_two_dice()
if total == 7:
sevens += 1
print(f"Sevens: {sevens}")
print(f"Proportion: {sevens / trials:.3f}")
Sevens: 16614
Proportion: 0.166
For simple cases like dice, exact probabilities are easy to calculate. But many real situations are complicated enough that simulation is the practical approach: What proportion of customers wait more than 5 minutes? How likely is a student to pass if they guess randomly on a 10-question quiz? How many days does a package typically take to arrive? Simulation answers these questions without advanced mathematics.
Randomness creates a testing challenge: if a program produces different results every time, how do you test it? There are two strategies.
Test that the result is in the right range or has the right type — not that it equals a specific value.
import random
result = random.randint(1, 10)
assert 1 <= result <= 10, "Result out of range"
A seed is a starting value that controls the sequence of random numbers generated. If you set the same seed before running, you get the same sequence every time. This makes randomness predictable for testing purposes.
import random
random.seed(42)
print(random.randint(1, 6)) # same value every run when seed is 42
print(random.randint(1, 6))
print(random.randint(1, 6))
Use random.seed(value) when you want to test a random program and need consistent output, when you want someone else to reproduce your simulation results exactly, or when you are debugging a problem that only appears with certain random values. Call random.seed() with no argument — or remove the seed call — when you want genuine unpredictability in the finished program.
random.seed(42) — sets the seed to 42; the same sequence of "random" numbers is produced every run. random.seed() — uses the current system time as the seed; different every run. No seed call — same as calling random.seed(); different every run.
Many useful programs need to work with dates and times: What day is today? How many days until a deadline? How long has it been since a file was created? What day of the week does a birthday fall on? Python's datetime module handles all of these.
The module provides several types. The two most useful for beginners are datetime.date (a calendar date: year, month, day) and datetime.datetime (a date and a time together). This chapter focuses on datetime.date.
import datetime
today = datetime.date.today()
print(today)
2025-03-14
Dates display in YYYY-MM-DD format by default. Section 24.7 shows how to change this.
import datetime
birthday = datetime.date(2007, 9, 22) # arguments: (year, month, day)
print(birthday) # 2007-09-22
import datetime
today = datetime.date.today()
print("Year:", today.year)
print("Month:", today.month)
print("Day:", today.day)
Year: 2025
Month: 3
Day: 14
The default date format (YYYY-MM-DD) is machine-readable but not always what you want to show a user. The strftime() method converts a date into a formatted string. Its name stands for "string format time."
import datetime
today = datetime.date.today()
formatted = today.strftime("%B %d, %Y")
print(formatted)
March 14, 2025
| Code | Meaning | Example output |
|---|---|---|
%Y | Four-digit year | 2025 |
%y | Two-digit year | 25 |
%m | Month as number (01–12) | 03 |
%B | Full month name | March |
%b | Abbreviated month name | Mar |
%d | Day of month (01–31) | 14 |
%A | Full weekday name | Friday |
%a | Abbreviated weekday name | Fri |
import datetime
d = datetime.date(2025, 9, 22)
print(d.strftime("%A, %B %d, %Y")) # Monday, September 22, 2025
print(d.strftime("%m/%d/%Y")) # 09/22/2025
print(d.strftime("%d %b %Y")) # 22 Sep 2025
Sometimes you receive a date as a string — from user input, a file, or a CSV — and need to convert it to a date object. datetime.datetime.strptime() does this. The second argument must match the format of the string exactly.
import datetime
date_string = "2025-09-22"
parsed = datetime.datetime.strptime(date_string, "%Y-%m-%d").date()
print(parsed) # 2025-09-22
print(parsed.year) # 2025
strftime() — takes a date object and converts it to a formatted string (date → string). strptime() — takes a date string and converts it back to a datetime object (string → date). The "f" stands for format (output); the "p" stands for parse (input).
The difference between two dates is called a duration. Python represents this with datetime.timedelta. Subtracting two date objects produces a timedelta. Its .days attribute gives the number of days in the duration.
import datetime
today = datetime.date.today()
new_year = datetime.date(2026, 1, 1)
days_away = new_year - today
print(f"Days until New Year: {days_away.days}")
You can add a timedelta to a date to find a future or past date.
import datetime
today = datetime.date.today()
one_week = datetime.timedelta(days=7)
next_week = today + one_week
print("Today:", today)
print("One week from now:", next_week)
# Also works for deadlines
deadline = datetime.date(2025, 12, 31)
buffer = datetime.timedelta(days=14)
reminder_date = deadline - buffer
print("Set reminder for:", reminder_date)
import datetime
start = datetime.date(2025, 9, 1)
end = datetime.date(2025, 12, 20)
duration = end - start
print(f"Semester length: {duration.days} days")
import datetime
start_time = datetime.datetime.now()
# some code that takes time
total = 0
for i in range(10_000_000):
total += i
end_time = datetime.datetime.now()
elapsed = end_time - start_time
print(f"Time elapsed: {elapsed}")
For precise measurement of short code segments, the standard library also provides time.perf_counter(), which is more accurate than datetime.datetime.now() for very short durations. For the exercises in this chapter, datetime is clear and readable.
Every simulation follows a similar four-step structure:
import random
def simulate_quiz(num_questions, num_choices):
"""Simulate a student guessing randomly on every question."""
correct = 0
for q in range(num_questions):
guess = random.randint(1, num_choices)
answer = 1 # correct answer is always choice 1
if guess == answer:
correct += 1
return correct
trials = 10000
total_correct = 0
for t in range(trials):
total_correct += simulate_quiz(10, 4)
average = total_correct / trials
print(f"Average score from random guessing: {average:.2f} out of 10")
print(f"Expected: 2.50 out of 10")
Average score from random guessing: 2.51 out of 10
Expected: 2.50 out of 10
Instead of averaging, you can collect how often each score appears — a distribution. Use a dictionary where the keys are scores and the values are counts.
import random
def simulate_quiz(num_questions, num_choices):
correct = 0
for q in range(num_questions):
if random.randint(1, num_choices) == 1:
correct += 1
return correct
trials = 10000
score_counts = {}
for t in range(trials):
score = simulate_quiz(10, 4)
if score not in score_counts:
score_counts[score] = 0
score_counts[score] += 1
print("Score | Count | Proportion")
print("------+-------+-----------")
for score in sorted(score_counts):
count = score_counts[score]
proportion = count / trials
print(f" {score:2d} | {count:5d} | {proportion:.3f}")
Score | Count | Proportion
------+-------+-----------
0 | 563 | 0.056
1 | 1878 | 0.188
2 | 2814 | 0.281
3 | 2499 | 0.250
4 | 1460 | 0.146
5 | 561 | 0.056
6 | 173 | 0.017
7 | 43 | 0.004
8 | 8 | 0.001
9 | 1 | 0.000
10 | 0 | 0.000
Most random guesses score 1, 2, or 3. Very few score above 5. This matches what we would expect from probability — the distribution reveals a pattern that a single average cannot.
Running a simulation produces numbers. Interpreting those numbers correctly requires care. Here are the key principles.
| Simulation is useful when… | Simulation is less useful when… |
|---|---|
| The exact probability requires complicated mathematics | The exact answer is easy to calculate directly |
| You want to model how a system behaves under different conditions | The model does not accurately represent the real situation |
| You want to test assumptions about randomness or fairness | The number of trials is too small to give reliable estimates |
| You are designing a game and want to check balance | The program runs too slowly to complete enough trials |
random and datetime modules are standard-library tools that require no installation. Both follow the same dot-notation pattern as other modules introduced earlier. Simulation is a programming technique, not a module — it uses randomness and loops to observe typical behavior over many trials.random.randint(a, b) returns a random integer in [a, b]. random.random() returns a float in [0.0, 1.0). random.uniform(a, b) returns a float in [a, b]. Tracing programs with randomness requires choosing representative values and testing near boundary conditions.random.choice(seq) returns one item. random.shuffle(lst) rearranges the list in place and returns None — assigning the result is a common mistake. random.sample(seq, k) returns a new list of k unique items without modifying the original.random.seed(value) to make sequences reproducible. Remove the seed in finished programs when genuine unpredictability is needed.datetime.date represents a calendar date. datetime.date.today() returns today's date. datetime.date(year, month, day) creates a specific date. The .year, .month, and .day attributes access individual parts.strftime(format) converts a date to a formatted string using codes like %Y, %B, %d, %A. strptime(string, format) parses a date string back into a datetime object.timedelta with a .days attribute. Adding or subtracting a timedelta produces a new date in the future or past. datetime.datetime.now() can time code execution.Read each block carefully. Answer the questions without running the code.
import random
total = 0
for i in range(5):
total += random.randint(1, 10)
print(total)
total?total?import random
items = ["apple", "banana", "cherry", "date"]
random.shuffle(items)
print(items[0])
"elderberry"? Explain."apple"?import datetime
start = datetime.date(2025, 1, 15)
end = datetime.date(2025, 3, 15)
gap = end - start
print(gap.days)
gap?gap instead of gap.days?import random
random.seed(99)
values = [random.randint(1, 100) for i in range(4)]
print(values)
random.seed(99)
values2 = [random.randint(1, 100) for i in range(4)]
print(values2)
values and values2 be equal? Explain why.random.seed(99) lines, would values and values2 still be equal? Explain.Trace this program. Assume random.randint(1, 6) returns 2 on the first call and 5 on the second call.
import random
def roll():
return random.randint(1, 6)
total = roll() + roll()
if total >= 7:
print("High roll:", total)
else:
print("Low roll:", total)
| Step | Expression evaluated | Value | Output |
|---|---|---|---|
| 1 | first call to roll() | 2 | |
| 2 | second call to roll() | 5 | |
| 3 | total | ? | |
| 4 | total >= 7 | ? | |
| 5 | branch taken | ? | ? |
Trace this simulation loop for 5 trials. Assume random.randint(1, 4) returns, in order: 3, 1, 4, 1, 2.
import random
correct = 0
for trial in range(5):
guess = random.randint(1, 4)
if guess == 1:
correct += 1
print(correct)
| Trial | guess | guess == 1 | correct after |
|---|---|---|---|
| 0 | |||
| 1 | |||
| 2 | |||
| 3 | |||
| 4 |
What does the program print?
Trace this date program step by step.
import datetime
event = datetime.date(2025, 6, 30)
today = datetime.date(2025, 6, 10)
days_away = event - today
if days_away.days <= 7:
print("Coming up soon!")
elif days_away.days <= 30:
print("This month")
else:
print("Further out")
| Step | Expression | Value |
|---|---|---|
| 1 | event | |
| 2 | today | |
| 3 | days_away | |
| 4 | days_away.days | |
| 5 | days_away.days <= 7 | |
| 6 | days_away.days <= 30 | |
| 7 | output |
Where output depends on randomness, state the possible range or list of possible outputs.
import datetime
d = datetime.date(2025, 11, 28)
print(d.strftime("%A, %B %d"))
Hint: November 28, 2025 is a Friday. What does this print exactly?
import random
random.seed(7)
choices = random.sample([10, 20, 30, 40, 50], 3)
print(len(choices))
print(type(choices))
What can you say for certain about the output, regardless of what the seed produces?
import datetime
today = datetime.date(2025, 3, 1)
delta = datetime.timedelta(days=365)
next_year = today + delta
print(next_year)
What does this print? (Think carefully about whether 2025 or 2026 is a leap year.)
import random
items = ["red", "blue"]
for i in range(3):
print(random.choice(items))
List all possible outputs. There are 2³ = 8 possible combinations. Write them out.
Each program below contains an error. Identify the error, explain what it causes, and write the corrected version.
import random
cards = ["A", "2", "3", "4", "5", "J", "Q", "K"]
cards = random.shuffle(cards)
print(cards[0])
What error does this cause, and on which line? Explain why, then write the correct version.
import datetime
birthday = datetime.date(1998, 13, 5)
print(birthday)
What error does this cause? What is the argument that causes the problem?
import random
total = 0
for i in range(10):
total = random.randint(1, 6)
print("Total:", total)
This runs without error but produces wrong results. What is the logic error? What should the assignment on line 5 look like?
import datetime
date1 = datetime.date(2025, 5, 1)
date2 = datetime.date(2025, 8, 15)
gap = date1 - date2
print("Days:", gap.days)
This runs without error. What does it print? Is the result a negative number? Is that meaningful? When would you want a negative timedelta?
import random
random.seed(42)
result = random.choice([])
print(result)
What error does this cause, and why? What rule does this illustrate about random.choice()?
Write clean, readable code with clear variable names.
random.choice() and a string of characters to generate a random 8-character password component. Include at least letters and digits in your character set. Print the result. Hint: build the result by iterating 8 times and appending each random character to a growing string.YYYY-MM-DD. Parse it into a datetime.date object. Calculate how many days until that birthday and print a message. Handle the case where the entered date is in the past with a clear message."2H", "KS", "AS" for 13 cards in one suit, or simply numbers 1–52. Shuffle the deck and print the first 5 cards as a hand.MM/DD/YYYY. Convert it to a datetime.date object and print which day of the week that date falls on.random.sample() to select 5 questions. Print the selected 5, numbered 1 through 5.random.random() and check if the result is less than 0.70.random.shuffle().y/n). Track the count of correct answers.strftime()), total cards, correct count, and percentage correct.=== Flashcard Session ===
Date: March 14, 2025
Card 1 of 8
Question: What keyword defines a function in Python?
Your answer: def
Answer: def
Correct? (y/n): y
Card 2 of 8
Question: What does len() return for an empty list?
Your answer: 0
Answer: 0
Correct? (y/n): y
...
=== Session Summary ===
Date: March 14, 2025
Cards studied: 8
Correct: 6
Score: 75.0%
random.sample() instead of always showing all cards.Before writing any code, answer these questions in comments at the top of your file: (1) What data structure will hold the flashcards? (2) How will you shuffle them? (3) What variables do you need during the session? (4) How will you calculate the final percentage? (5) What format will you use to display the date?
pick_winner(names) that takes a list of names and returns one randomly selected name. Write a second function called run_giveaway(names, num_winners) that uses random.sample() to select multiple unique winners. Test both functions with a list of at least 6 names.YYYY-MM-DD format), converts each to a datetime.date object, and prints the dates in chronological order from earliest to latest. Hint: datetime.date objects support comparison operators like < and >, so you can sort a list of them.simulate_guessing(num_questions, num_choices, trials) that simulates a student guessing randomly on a quiz for the given number of trials and returns the average score as a float. Test it with several different values of num_questions and num_choices and print the results in a table.random.randint() to pick a month (1–12) and a day. To keep it simple, allow only days 1–28 (valid for every month), or handle different month lengths for a greater challenge.random.choice() and random.sample()?random.shuffle() return None instead of the shuffled list?datetime.date.today() return? What type is the result?strftime() and strptime()?datetime.date objects, what type is the result? How do you get the number of days from it?cards = ["A", "B", "C", "D"]
picked = random.shuffle(cards)
print(picked[0])
deal_card(deck) that removes and returns a random card from a list. What would you test using assert statements? List at least three things you could check without knowing the exact card drawn.random function you would use and why. Write the key line of Python that performs the selection. What happens if the class has 25 students?random.randint(a, b) to generate a random integer in a range.random.random() for a random float in [0.0, 1.0).random.uniform(a, b) for a random float in any range.random.choice() to pick one item from a sequence.random.shuffle() modifies in place and returns None.random.sample() to select multiple unique items without modifying the original.datetime.date.today() and create specific dates.strftime() with common codes.strptime()..days.Chapter 24 introduced randomness, dates, and simulation — tools for modeling uncertainty and working with calendar data. Chapter 25 takes a different direction and asks: how can Python automate practical, repetitive work? You will learn to write programs that process many files, generate reports, handle command-line arguments, use configuration values, and perform repetitive tasks efficiently. The random module from this chapter and the file-handling skills from Chapters 21–22 both reappear in the automation context. As always, the focus is on writing automation that is safe, reversible, and well-tested before being run on real data.