# Date and Time

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Reference Types
Source: https://www.devtutorials.ai/learn/python/reference-types/date-and-time

> The datetime module: date, time and datetime objects, strftime and strptime, timedelta arithmetic, timestamps, and naive against aware moments.

A school runs on dates.

Term starts on a date. Homework is due on a date. A report card says it was printed on a date. Somebody asks how many days are left until the exam, and somebody else asks whether a submission came in before the deadline.

## What You Will Learn

In this lesson, you will learn:

- Why dates need their own type, and what `datetime` gives you
- The three classes you will use: `date`, `time` and `datetime`
- How to make one, and how to ask for today's
- How to turn a date into text, and text back into a date
- How to add days to a date and find the gap between two of them
- What a timestamp is, and when you need one

By the end of this lesson, you will be able to create dates, times and moments, show them in any form, read them back from text, work out gaps and lengths, and handle the time zone traps that catch beginners out.

## Introduction to Date and Time

### Why Date and Time Are Important

Text cannot answer those questions. `"14/03/2026"` is just characters — Python cannot add thirty days to it or tell you it comes after `"05/01/2026"`. Something built for dates is needed, and here is what it looks like:

```python runnable title="A date knows things text does not"
from datetime import date, timedelta

exam = date(2026, 3, 14)

print(exam)
print("Year:", exam.year)
print("Day of the week:", exam.strftime("%A"))
print("Thirty days later:", exam + timedelta(days=30))
```

A date is a value Python understands, not text that looks like a date.

Three questions come up in almost every program that handles dates, and none can be answered with text.

**Which came first?** Text is compared character by character, so the comparison depends on how the date happens to be written rather than on what it means.

**How far apart are they?** Subtracting text is not possible at all.

**What is thirty days from now?** That needs month lengths and leap years.

```python runnable title="Text comparison gets it wrong"
january_text = "05/01/2026"
march_text = "04/03/2026"

print("As text, is January later than March?", january_text > march_text)

from datetime import date

january = date(2026, 1, 5)
march = date(2026, 3, 4)

print("As dates, is January later than March?", january > march)
print("Days between them:", (march - january).days)
```

As text the answer is `True`, which is wrong: 5 January is not after 4 March. Python only compared `"05"` with `"04"` and stopped there, because `5` is the bigger character. As dates the answer is right, and the gap comes out as well.

The dangerous part is that the same comparison is sometimes right by accident. Written as `"05/01/2026"` against `"14/03/2026"` it gives the correct answer, for the wrong reason — which is how this bug survives testing.

### Date and Time in Python

Dates live in the `datetime` module, which ships with Python. There is nothing to install.

<div className="tscroll">

| Class | Holds | Use it for |
|---|---|---|
| `date` | year, month, day | a birthday, a due date, a term start |
| `time` | hour, minute, second | a class period, an alarm |
| `datetime` | both together | a submission, a log entry |
| `timedelta` | a length of time | "thirty days", "two hours" |

</div>

```python runnable title="All four, side by side"
from datetime import date, time, datetime, timedelta

print(date(2026, 3, 14))
print(time(14, 30))
print(datetime(2026, 3, 14, 14, 30))
print(timedelta(days=30))
```

Now that you know the four main date and time types, let's look at how they are imported and used in real programs.

## Datetime Module

### Working with Dates

Import what you need by name. The module and the class are both called `datetime`, which is confusing enough that importing by name is worth the habit.

```python runnable title="Importing by name avoids the confusion"
from datetime import date, datetime

print(date(2026, 3, 14))
print(datetime(2026, 3, 14, 9, 0))

import datetime as dt

print(dt.date(2026, 3, 14))
```

`from datetime import datetime` means the name `datetime` is now the class. `import datetime` means it is the module, and the class is `datetime.datetime`.

Anything to do with a day, a month and a year is `date` territory:

### Working with Times

```python runnable title="A time on its own"
from datetime import time

period = time(9, 30)

print(period)
print(period.hour, period.minute)
```

A `time` holds no date. It is the right type for "when does the first period start", which is true every day.

### Date and Time Formatting

Printing a date gives the ISO form, `YYYY-MM-DD`. `strftime()` gives you any other form.

```python runnable title="One date, many ways to show it"
from datetime import date

exam = date(2026, 3, 14)

print(exam)
print(exam.strftime("%d/%m/%Y"))
print(exam.strftime("%d %B %Y"))
print(exam.strftime("%A, %d %b %Y"))
```

Formatting is covered fully further down this page.

Once a date has been built, the next step is reading and changing its parts:

The datetime module provides several types, but dates are the starting point because they are the simplest.

## Date Objects

### date Class

A `date` holds a year, a month and a day. Nothing else.

### Creating Dates

```text
date(year, month, day)
```

All three are required, and they are checked:

```python runnable expect-error title="A day that does not exist"
from datetime import date

print(date(2026, 2, 30))
```

`ValueError: day is out of range for month`. February 2026 has 28 days, and Python knows it.

```python runnable title="Leap years are handled for you"
from datetime import date

print(date(2024, 2, 29))
print(date(2024, 12, 31).timetuple().tm_yday, "days in 2024")
print(date(2026, 12, 31).timetuple().tm_yday, "days in 2026")
```

2024 is a leap year and 2026 is not, and nothing had to be worked out by hand.

### Current Date

`date.today()` gives today's date on the machine running the program:

```python runnable varies title="Today, wherever this runs"
from datetime import date

today = date.today()

print(today)
print("Year:", today.year)
print("Weekday name:", today.strftime("%A"))
```

The output changes every day, which is the point. Anything that needs a fixed date for testing should be given one rather than reading the clock.

### Accessing Year, Month, Day

The three parts are always there, waiting to be read:

```python runnable title="The three parts"
from datetime import date

exam = date(2026, 3, 14)

print("Year:", exam.year)
print("Month:", exam.month)
print("Day:", exam.day)
```

These are plain numbers, so they can be used in arithmetic or in an f-string.

```python runnable title="Using the parts"
from datetime import date

exam = date(2026, 3, 14)

print(f"The exam is on day {exam.day} of month {exam.month}")
print("Is it in the first half of the year?", exam.month <= 6)
```

They cannot be assigned, because a date is immutable like a string:

```python runnable expect-error title="A date cannot be changed in place"
from datetime import date

exam = date(2026, 3, 14)
exam.year = 2027
```

`replace()` gives you a new date instead, which is covered below.

### date Methods

Here is the set that comes with every date:

<div className="tscroll">

| Method | What it gives | Example result |
|---|---|---|
| `weekday()` | day of week, Monday is 0 | `5` for a Saturday |
| `isoweekday()` | day of week, Monday is 1 | `6` for a Saturday |
| `isoformat()` | the `YYYY-MM-DD` text | `2026-03-14` |
| `strftime(fmt)` | text in any form you choose | `14 March 2026` |
| `replace(...)` | a new date with parts changed | `2027-03-14` |
| `toordinal()` | days since year 1 | `739690` |

</div>

```python runnable title="The date methods at work"
from datetime import date

exam = date(2026, 3, 14)

print("weekday():", exam.weekday())
print("isoweekday():", exam.isoweekday())
print("Name:", exam.strftime("%A"))
print("isoformat():", exam.isoformat())
print("replace(year=2027):", exam.replace(year=2027))
print("Original is untouched:", exam)
```

`weekday()` counting from 0 and `isoweekday()` counting from 1 is a classic source of off-by-one bugs. Pick one and be deliberate.

```python runnable title="Is it a weekend?"
from datetime import date

for day in [date(2026, 3, 13), date(2026, 3, 14), date(2026, 3, 15)]:
    print(day.strftime("%A"), "- weekend?", day.weekday() >= 5)
```

`date.fromisoformat()` goes the other way, turning standard text back into a date:

```python runnable title="Reading a standard date from text"
from datetime import date

print(date.fromisoformat("2026-03-14"))
print(date.fromisoformat("2026-03-14").strftime("%d %B %Y"))
```

A date holds no time of day. For that, there is a second class:

## Time Objects

### time Class

A `time` holds an hour, a minute, a second and microseconds. It has no date attached.

### Creating Times

```text
time(hour, minute, second, microsecond)
```

Every part is optional and starts at 0.

```python runnable title="Times, with parts left out"
from datetime import time

print(time(9))
print(time(9, 30))
print(time(14, 30, 5))
print(time())
```

The hour runs from 0 to 23:

```python runnable expect-error title="There is no 25 o'clock"
from datetime import time

print(time(25, 0))
```

### Current Time

A `time` has no `today()`. You take the current moment and ask for its time part.

```python runnable varies title="The time right now"
from datetime import datetime

now = datetime.now()

print("Full moment:", now)
print("Just the time:", now.time())
print("Hour:", now.hour)
```

### Accessing Time Components

```python runnable title="The parts of a time"
from datetime import time

period = time(14, 30, 5)

print("Hour:", period.hour)
print("Minute:", period.minute)
print("Second:", period.second)
print("Microsecond:", period.microsecond)
```

### time Methods

```python runnable title="What a time can do"
from datetime import time

period = time(14, 30, 5)

print("isoformat():", period.isoformat())
print("24-hour:", period.strftime("%H:%M"))
print("12-hour:", period.strftime("%I:%M %p"))
print("replace(hour=9):", period.replace(hour=9))
```

Times compare, which is how you check a deadline:

```python runnable title="Comparing times"
from datetime import time

submitted = time(9, 45)
deadline = time(10, 0)

print("On time?", submitted <= deadline)
print("Late?", submitted > deadline)
```

Times cannot be added together. "Half past nine plus half past ten" means nothing — adding a **length** of time is the `timedelta` job further down.

For anything that happened at a moment — a submission, a log entry — a date and a time are needed together, which is the third class:

## Datetime Objects

### datetime Class

A `datetime` holds a date and a time in one value. It is what you use for anything that happened at a moment.

### Current Date and Time

```python runnable varies title="Now"
from datetime import datetime

now = datetime.now()

print(now)
print("Date part:", now.date())
print("Time part:", now.time())
```

`datetime.today()` does almost the same thing. `now()` is the one to use, because it can also take a time zone.

### Creating Datetime Objects

```text
datetime(year, month, day, hour, minute, second)
```

The date parts are required; the time parts start at 0.

```python runnable title="Building a moment"
from datetime import datetime

submitted = datetime(2026, 3, 14, 9, 45)

print(submitted)
print(datetime(2026, 3, 14))
```

### Datetime Components

Everything from both classes is available on one value.

```python runnable title="All the parts of a moment"
from datetime import datetime

submitted = datetime(2026, 3, 14, 9, 45, 30)

print("Year:", submitted.year)
print("Month:", submitted.month)
print("Day:", submitted.day)
print("Hour:", submitted.hour)
print("Minute:", submitted.minute)
print("Second:", submitted.second)
```

### datetime Methods

```python runnable title="Pulling a moment apart and putting it back"
from datetime import datetime, date, time

submitted = datetime(2026, 3, 14, 9, 45)

print("date():", submitted.date())
print("time():", submitted.time())
print("replace(hour=10):", submitted.replace(hour=10))
print("combine():", datetime.combine(date(2026, 3, 14), time(9, 0)))
print("isoformat():", submitted.isoformat())
print("strftime():", submitted.strftime("%d %B %Y at %I:%M %p"))
```

`combine()` is the reverse of `date()` and `time()`: it builds one moment from a date and a time you already have.

Datetimes compare and subtract, exactly as dates do:

```python runnable title="Comparing two moments"
from datetime import datetime

deadline = datetime(2026, 3, 14, 10, 0)
submitted = datetime(2026, 3, 14, 9, 45)

print("On time?", submitted <= deadline)
print("Minutes to spare:", (deadline - submitted).total_seconds() / 60)
```

Building, reading and showing a date all involve one pair of methods, worth a section of their own:

## Formatting and Parsing

Two methods, easy to mix up:

- **`strftime()`** turns a date into text. Think *f* for **format**.
- **`strptime()`** turns text into a date. Think *p* for **parse**.

### strftime()

```python runnable title="A date shown many ways"
from datetime import datetime

moment = datetime(2026, 3, 14, 14, 30)

print(moment.strftime("%d/%m/%Y"))
print(moment.strftime("%Y-%m-%d"))
print(moment.strftime("%d %B %Y"))
print(moment.strftime("%A, %d %b"))
print(moment.strftime("%I:%M %p"))
print(moment.strftime("Printed on %d %B %Y at %H:%M"))
```

Anything that is not a format code is kept as it is, so you can build a whole sentence.

### strptime()

`strptime()` needs the text **and** a pattern describing its shape.

```python runnable title="Reading text as a date"
from datetime import datetime

print(datetime.strptime("14/03/2026", "%d/%m/%Y"))
print(datetime.strptime("2026-03-14 09:45", "%Y-%m-%d %H:%M"))
print(datetime.strptime("14 March 2026", "%d %B %Y").date())
```

The pattern must match the text exactly:

```python runnable expect-error title="A pattern that does not fit"
from datetime import datetime

print(datetime.strptime("14-03-2026", "%d/%m/%Y"))
```

`ValueError: time data '14-03-2026' does not match format '%d/%m/%Y'` — the text uses dashes and the pattern expects slashes.

This is also where day-first and month-first formats bite:

```python runnable title="The same text, two readings"
from datetime import datetime

text = "03/04/2026"

print("Day first: ", datetime.strptime(text, "%d/%m/%Y").strftime("%d %B"))
print("Month first:", datetime.strptime(text, "%m/%d/%Y").strftime("%d %B"))
```

Same six digits, two different dates, and no error either way. When you read dates from a file, know which form they are in.

Both strftime() and strptime() use the same set of formatting codes. The table below lists the ones you will use most often.

### Common Format Codes

Here is the set worth keeping to hand:

<div className="tscroll">

| Code | Means | Example |
|---|---|---|
| `%Y` | four-digit year | `2026` |
| `%y` | two-digit year | `26` |
| `%m` | month as a number | `03` |
| `%B` | month name | `March` |
| `%b` | short month name | `Mar` |
| `%d` | day of the month | `14` |
| `%A` | weekday name | `Saturday` |
| `%a` | short weekday name | `Sat` |
| `%H` | hour, 24-hour | `14` |
| `%I` | hour, 12-hour | `02` |
| `%M` | minute | `30` |
| `%S` | second | `05` |
| `%p` | AM or PM | `PM` |
| `%%` | a literal percent sign | `%` |

</div>

```python runnable title="Every code above, on one date"
from datetime import datetime

moment = datetime(2026, 3, 14, 14, 30, 5)

for code in ["%Y", "%y", "%m", "%B", "%b", "%d", "%A", "%a", "%H", "%I", "%M", "%S", "%p"]:
    print(code, "->", moment.strftime(code))
```

Capital `%M` is minutes; small `%m` is the month. Capital `%H` is the 24-hour clock; capital `%I` is the 12-hour one and needs `%p` beside it to be unambiguous.

Dates can also be moved forward and back, and measured against each other:

## Date and Time Arithmetic

### timedelta

A `timedelta` is a **length** of time, not a point in time. "Thirty days" rather than "the fourteenth of March".

```python runnable title="Lengths of time"
from datetime import timedelta

print(timedelta(days=30))
print(timedelta(weeks=2))
print(timedelta(hours=3, minutes=30))
print(timedelta(days=1, hours=12).total_seconds())
```

It accepts weeks, days, hours, minutes, seconds, milliseconds and microseconds — but **not months or years**, because those are not fixed lengths. A month can be 28 to 31 days.

```python runnable title="Why there is no months argument"
from datetime import date

print("January has", (date(2026, 2, 1) - date(2026, 1, 1)).days, "days")
print("February has", (date(2026, 3, 1) - date(2026, 2, 1)).days, "days")
```

To move by a month, change the month number with `replace()`, or add the right number of days deliberately.

### Adding Dates and Times

```python runnable title="Moving forwards and backwards"
from datetime import date, datetime, timedelta

exam = date(2026, 3, 14)

print("A week later:", exam + timedelta(weeks=1))
print("Thirty days later:", exam + timedelta(days=30))
print("A week earlier:", exam - timedelta(weeks=1))

submitted = datetime(2026, 3, 14, 9, 45)
print("Two and a half hours later:", submitted + timedelta(hours=2, minutes=30))
```

Month ends and leap years are handled for you:

```python runnable title="Crossing a month end"
from datetime import date, timedelta

print(date(2026, 1, 31) + timedelta(days=1))
print(date(2024, 2, 28) + timedelta(days=1))
print(date(2026, 2, 28) + timedelta(days=1))
print(date(2026, 12, 31) + timedelta(days=1))
```

2024 gets a 29 February and 2026 does not, with nothing to remember.

### Difference Between Dates

Subtracting two dates gives a `timedelta`.

```python runnable title="How far apart?"
from datetime import date

term_start = date(2026, 1, 5)
exam = date(2026, 3, 14)

gap = exam - term_start

print(gap)
print("Days:", gap.days)
print("Weeks:", gap.days // 7)
```

Subtracting two datetimes gives a length including the time of day:

```python runnable title="A gap measured in hours"
from datetime import datetime

start = datetime(2026, 3, 14, 9, 0)
end = datetime(2026, 3, 14, 12, 30)

gap = end - start

print(gap)
print("Hours:", gap.total_seconds() / 3600)
```

`.days` is whole days only; `.total_seconds()` is the whole length as a number, and is what you want for anything shorter than a day.

```python runnable title="Where .days misleads"
from datetime import datetime

gap = datetime(2026, 3, 14, 23, 0) - datetime(2026, 3, 14, 9, 0)

print("Length:", gap)
print(".days says:", gap.days)
print("total_seconds() says:", gap.total_seconds() / 3600, "hours")
```

Fourteen hours gives `.days` of 0, because it is not a whole day.

There is one more way to hold a moment — as a single plain number — which is the last idea of the lesson:

## Timestamps

### timestamp()

A **timestamp** is a single number: the seconds since 1 January 1970, a moment known as the epoch. It is how computers store and exchange moments.

```python runnable title="A moment as one number"
from datetime import datetime, timezone

moment = datetime(2026, 3, 14, 12, 0, tzinfo=timezone.utc)

print(moment)
print(moment.timestamp())
```

One number, no formatting and no ambiguity, which is why files and databases use them.

### fromtimestamp()

```python runnable title="Turning the number back into a moment"
from datetime import datetime, timezone

stamp = 1773489600.0

print(datetime.fromtimestamp(stamp, tz=timezone.utc))
print(datetime.fromtimestamp(stamp, tz=timezone.utc).strftime("%d %B %Y, %H:%M"))
```

A timestamp with no time zone attached is read using the machine's own zone, which means the same number gives different answers on different computers:

```python runnable varies title="The same number, read by this machine"
from datetime import datetime, timezone

stamp = 1773489600.0

print("As UTC:        ", datetime.fromtimestamp(stamp, tz=timezone.utc))
print("As this machine:", datetime.fromtimestamp(stamp))
```

If those two lines differ, this machine is not on UTC. That is exactly the kind of difference that turns into a bug nobody can reproduce.

A `datetime` with no time zone is called **naive**, and `.timestamp()` on one uses whatever zone the machine happens to be in. When a moment matters — a deadline, a log entry, anything stored — attach `timezone.utc`.

```python runnable title="Naive against aware"
from datetime import datetime, timezone

naive = datetime(2026, 3, 14, 12, 0)
aware = datetime(2026, 3, 14, 12, 0, tzinfo=timezone.utc)

print("Naive:", naive, "- tzinfo:", naive.tzinfo)
print("Aware:", aware, "- tzinfo:", aware.tzinfo)
```

```python runnable expect-error title="Naive and aware do not mix"
from datetime import datetime, timezone

naive = datetime(2026, 3, 14, 12, 0)
aware = datetime(2026, 3, 14, 12, 0, tzinfo=timezone.utc)

print(aware - naive)
```

`TypeError: can't subtract offset-naive and offset-aware datetimes`. Python refuses to guess, which is the right behaviour. Keep a program consistently one or the other.

Before moving on, here are some of the mistakes beginners make most often with dates and times.

## Common mistakes

Each row is one mistake. The middle column is what you will actually see on screen.

<div className="tscroll">

| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
| Comparing dates as text | January looks later than March | Text compares character by character | Use `date` objects |
| `import datetime` then `datetime(...)` | `TypeError: 'module' object is not callable` | The module and the class share a name | `from datetime import datetime` |
| `%M` where `%m` was meant | Minutes printed as the month | Capital is minutes, small is month | Check the case of every code |
| A `strptime` pattern that does not match | `ValueError: time data ... does not match format` | The pattern must match exactly | Make the separators and order match |
| Day-first text read as month-first | A real but wrong date, with no error | `03/04` is valid either way | Know the source's format |
| Expecting `timedelta(months=1)` | `TypeError: unexpected keyword argument 'months'` | A month has no fixed length | Use `replace()`, or a number of days |
| Using `.days` for a gap under a day | `0` for a fourteen-hour gap | `.days` counts whole days only | `total_seconds()` |
| Assigning to `.year` | `AttributeError: attribute is read-only` | Dates are immutable | `exam.replace(year=2027)` |
| `.timestamp()` on a naive datetime | A number that differs between machines | The machine's own zone is used | Attach `timezone.utc` |
| Subtracting naive from aware | `TypeError: can't subtract offset-naive and offset-aware` | Python will not guess a zone | Keep both the same kind |
| A date that does not exist | `ValueError: day is out of range for month` | 30 February is checked and refused | Check the real month length |
| Mixing `weekday()` and `isoweekday()` | Everything off by one | One counts from 0, the other from 1 | Pick one and stay with it |

</div>

## What to Remember

- **A date is a value Python understands, not text that looks like a date.** Text compares and subtracts wrongly.
- `date` holds a day, `time` holds a clock time, `datetime` holds both, `timedelta` holds a length.
- `from datetime import datetime` imports the class; `import datetime` imports the module.
- `date.today()` and `datetime.now()` read the machine's clock, so their output changes every run.
- Dates are immutable — `replace()` gives you a new one.
- **`strftime()` formats a date as text; `strptime()` parses text into a date** and needs a pattern that matches exactly.
- `%m` is the month and `%M` is minutes. `%B` is the month name, `%A` the weekday name.
- `timedelta` has no `months` or `years`, because neither has a fixed length.
- Subtracting two dates gives a `timedelta`: `.days` for whole days, `.total_seconds()` for anything shorter.
- A timestamp is seconds since 1970, and **a naive datetime's timestamp depends on the machine's time zone**. Attach `timezone.utc` when the moment matters.

Next: **Classes**
