Free Handbook · Runs in your browser

Regex, Date & Time

Regular expressions for the patterns you actually meet, then datetime end to end: creating, formatting with strftime, parsing with strptime, arithmetic with timedelta, timestamps, time zones done right — and the two errors that hit every data pipeline: the format mismatch and naive-versus-aware.

0 / 108 lessons🔥 0 day streak
ShareXLinkedIn

Module 10 · what you'll be able to do

  • Search, extract and replace with re, using groups and the four functions that matter
  • Create dates and datetimes, format them and parse them with the right directives
  • Add and subtract time with timedelta and compare dates
  • Convert between timestamps and datetimes without an off-by-timezone bug
  • Use zoneinfo for real time zones and never mix naive and aware datetimes
01

Regular expressions

A regex describes a pattern of text. You need four functions — search (first match anywhere), match (at the start), findall (every match), sub (replace) — and about a dozen symbols. Always write patterns as raw strings (r"…") so backslashes reach the regex engine intact.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
user=ada id=4021 | ada 4021
{'date': '2026-09-20', 'time': '14:03:11', 'level': 'ERROR'}
['10.0.0.7']
['user', 'id', 'msg', 'ip']
2026-09-20 14:03:11 ERROR user=ada id=**
due 20/09/2026
'[email protected]'            True
'not an email'               False
'[email protected]'    True
['a', 'b', 'c', 'd']
Your turn
Extract every #hashtag from "loving #python and #regex, not #c++ though". Then make the pattern stop at the +.
SymbolMatchesExample
.any character except newlinea.c → abc, a-c
\d \w \sdigit · word char (letters, digits, _) · whitespacecapitals negate: \D = not a digit
* + ?0 or more · 1 or more · 0 or 1colou?r → color, colour
{n} {n,m}exactly n · between n and m\d{4} → a year
[abc] [^abc] [a-z]a set · not in the set · a range
^ $start · end of string (or line with re.M)
(…) (?P<name>…)a group · a named groupm.group(1), m["name"]
a|ba or bcat|dog
*? +?lazy: as few as possible<.+?> matches one tag, not the whole line
Greedy by default
<.+> on <b>hi</b> matches the whole thing, because + grabs as much as it can and still succeeds. Add ? for the shortest match. And do not parse HTML with regex at all — use an HTML parser.
Quick check

Which call finds a pattern anywhere in the string, not just at the start?

02

datetime: dates, times and arithmetic

The datetime module has four classes you use: date, time, datetime (both), and timedelta (a duration). Dates are comparable and subtractable; adding a timedelta moves them. Almost every date bug in the wild comes from strings — so convert to these objects at the edge of your program and back to strings only for output.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
2026-09-20 2026 9 6 Sunday
2026-09-20 14:30:00 | 2026-09-20 | 14:30:00 | 14
2026-11-04
96 days, 0:00:00
96 days to go
2026-09-22 02:30:00
604800.0 168.0
True 2026-09-20
2026-09-01 2026-09-20 14:00:00
2026-09-20T14:30:00 True
Your turn
Compute the date of the next Friday after d, then the number of days between 2026-03-01 and 2026-02-01 (watch the leap year).
now() and today()
datetime.now() and date.today() give the current moment on the machine's clock. They are not used in the examples here because their output changes every second — but that is exactly why tests should inject the clock rather than call them directly.
03

Formatting and parsing: strftime and strptime

strftime (string-from-time) turns a datetime into text using directives like %Y; strptime (string-parse-time) does the reverse with the same directives. The directives must describe the text exactly — every dash, colon and space — which is where the ValueError below comes from.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
2026-09-20
20/09/2026 14:05
Sunday, 20 September 2026 at 02:05 PM
Sep 20
20260920_140509
2026-09-20 14:05:00
2026-09-20
2026-09-20 14:05:09
DirectiveMeaningExample
%Y %m %dyear (4) · month (01–12) · day (01–31)2026 09 20
%H %M %Shour (00–23) · minute · second14 05 09
%I %p12-hour clock · AM/PM02 PM
%A %a %B %bweekday · short · month name · shortSunday Sun September Sep
%fmicroseconds000000
%z %ZUTC offset · zone name+0000 UTC
%j %Uday of year · week of year
Error you will hit

ValueError: time data '2026-09-20' does not match format '%d/%m/%Y'

python
from datetime import datetime
datetime.strptime("2026-09-20", "%d/%m/%Y")
Traceback (most recent call last):
  File "your code", line 2, in <module>
    datetime.strptime("2026-09-20", "%d/%m/%Y")
  ...
ValueError: time data '2026-09-20' does not match format '%d/%m/%Y'
Why the interpreter said that

The parser walked the format and the text together and hit a mismatch: it expected a day then a slash and found "2026-". The message quotes both, so read them side by side. In pipelines this usually means the upstream system changed its format, or one row out of a million is different — the practice problem below is exactly that.

The fix

Match the format to the actual text, or use fromisoformat for ISO 8601, or try several formats in order.

python
from datetime import datetime

def parse_any(text):
    for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%b %d %Y"):
        try:
            return datetime.strptime(text, fmt).date()
        except ValueError:
            continue
    raise ValueError(f"unrecognised date: {text!r}")

print(parse_any("2026-09-20"), parse_any("20/09/2026"))
04

Timestamps and time zones

A Unix timestamp is seconds since 1970-01-01 00:00 UTC. It has no time zone problem because it is an instant, not a wall-clock reading. Wall-clock datetimes come in two kinds: naive (no tzinfo — "14:30, somewhere") and aware (tzinfo attached — "14:30 in Kolkata"). Mixing them is an error; treating a naive one as UTC when it was local is a silent bug that shifts every record by hours. Rule: store and compute in UTC, convert to a zone only for display.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
2026-09-21 14:13:20+00:00 UTC
True
2026-09-21 19:43 IST | 2026-09-21 10:13 EDT
True
2026-09-21 08:00:00+00:00
2026-09-21T09:00:00+01:00
None → naive
TypeError: can't subtract offset-naive and offset-aware datetimes
-1 day, 23:00:00
pythonreal zones — on your machine, with zoneinfo
from datetime import datetime, timezone
from zoneinfo import ZoneInfo          # standard library since 3.9

utc = datetime(2026, 9, 21, 6, 13, 20, tzinfo=timezone.utc)
print(utc.astimezone(ZoneInfo("Asia/Kolkata")))      # 2026-09-21 11:43:20+05:30
print(utc.astimezone(ZoneInfo("America/New_York")))  # 2026-09-21 02:13:20-04:00

# ZoneInfo knows daylight saving; a fixed offset does not:
london = ZoneInfo("Europe/London")
print(datetime(2026, 1, 15, 9, tzinfo=london).utcoffset())   # 0:00:00  (GMT)
print(datetime(2026, 7, 15, 9, tzinfo=london).utcoffset())   # 1:00:00  (BST)

Fixed offsets are what the browser example uses because the IANA database is not bundled here. In real code use ZoneInfo — it handles daylight saving and historical changes. On Windows, pip install tzdata supplies the database.

Error you will hit

TypeError: can't compare offset-naive and offset-aware datetimes

python
from datetime import datetime, timezone

deadline = datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc)   # aware
submitted = datetime(2026, 12, 31, 20, 0)                        # naive — from a DB column
print(submitted <= deadline)
Traceback (most recent call last):
  File "your code", line 5, in <module>
    print(submitted <= deadline)
TypeError: can't compare offset-naive and offset-aware datetimes
Why the interpreter said that

Python refuses to guess which zone the naive one is in — because guessing wrong would silently move your deadline by hours. The naive value usually came from a database column without a time zone, a CSV, or datetime.now() called without tz=.

The fix

Make everything aware at the boundary where it enters your program, in the zone it was actually recorded in, then convert to UTC.

python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

deadline = datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc)
submitted = datetime(2026, 12, 31, 20, 0).replace(tzinfo=ZoneInfo("Asia/Kolkata"))
print(submitted.astimezone(timezone.utc), submitted <= deadline)
What the practice problem tests
Real feeds mix three formats — ISO strings with an offset, naive strings that are "known" to be UTC, and integer epochs (sometimes in milliseconds). Normalising all three to one aware UTC datetime is the most common first task on a data pipeline, and it is the problem below.
05

The time module and sleep

time is the lower-level sibling: time.time() is the current Unix timestamp as a float, time.perf_counter() is the clock to use for measuring how long code takes (monotonic, high resolution), and time.sleep(seconds) pauses the program. Sleeping in a retry loop should back off — 1 s, 2 s, 4 s — never hammer a failing service at a fixed rate.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
1999999000000 computed in under a second: True
[1.0, 2.0, 4.0, 8.0, 16.0, 30.0]
slept
float True
Quick check

Which clock should you use to time how long a function takes?

Finish the Python handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.