Definition
A date inside a program is almost never there just to be displayed. You want to know whether it has passed, compare it with another one, add a delay to it. As long as it stays text, none of those questions has a simple answer: the program sees a run of characters, not a point in the calendar.
datetime is the standard library module that solves that problem. It represents a date, a time, or both, as an object that knows its own place in time. No installation with pip is needed, an import is enough.
from datetime import datetime, timedelta
now = datetime.now()
delivery = now + timedelta(days=45) # 45 days later, leap years included
print(delivery.strftime("%d/%m/%Y")) # 07/09/2026, for instanceThe whole service rendered sits in the second line. With a string, adding forty-five days means knowing the length of every month and the fate of February. With an object, it is an addition, and the module takes care of the rest.
The four objects to know
The module exposes several classes, but four of them cover almost every real need. In the table below, the middle column is the one that decides: the choice of object depends only on what you need to hold on to.
| Object | What it holds | Typical use |
|---|---|---|
| date | A year, a month, a day | A birth date, a due date |
| time | Hours, minutes, seconds | An opening time |
| datetime | Both together | The timestamp of an event |
| timedelta | A duration, not a moment | The gap between two dates |
An arithmetic ties these objects together, and it explains most beginner mistakes: subtracting two dates gives a duration, therefore a timedelta, and adding a duration to a date gives a date back. Adding two dates together, on the other hand, points to no moment at all, and Python refuses it.
These objects also belong to everything immutable in Python. An operation never changes the object it started from, it returns a new one: calling replace on a date without catching the result therefore changes nothing whatsoever, and without a single error message.
What it replaces
Two habits usually come before discovering the module. Each one works for a while, then breaks on a precise case.
The first is keeping dates as text. For display it is perfect; for comparison it is wrong. "31/01/2026" reads as greater than "01/02/2026", because two strings are compared character by character: the 3 against the 0 settles it before the month has even been looked at. Sorting a list of dates held as text therefore produces an order with nothing to do with the calendar.
One exception explains why some people never run into the problem: the ISO 8601 format, which writes the year, then the month, then the day, lines the order of the characters up with the order of time. The accident is a happy one, but it disappears as soon as a file arrives in the French format.
The second habit is running everything through the time module and its timestamps, those counts of seconds elapsed since 1970. The arithmetic is correct, which is what makes the habit stick. But a ten digit number cannot be read back and says nothing about the zone, where datetime keeps the same precision while staying legible in a journal produced by logging.
Writing a date, then reading it back
Two operations then keep coming back: showing the date to a human, and reading back what a human or a file has written. Two methods with almost identical names take care of that. The f in strftime stands for format: it starts from an object and builds text. The p in strptime stands for parse: it does exactly the opposite.
from datetime import datetime
# From text to object: the pattern describes the text received
meeting = datetime.strptime("24/07/2026 09:30", "%d/%m/%Y %H:%M")
# From object to text: the pattern describes the text wanted
print(meeting.strftime("%d %B %Y"))
# In ISO format: no pattern to write at all
print(datetime.fromisoformat("2026-07-24T09:30:00"))In both directions, the pattern has to describe the text character for character, separators included. A dash where the file carries a slash raises a ValueError, the leading cause of failure on a data import: the program swallows ten thousand rows without blinking, then stops dead on the three that depart from the announced format.
The last line shows the way out. Between two programs, the isoformat and fromisoformat pair removes the question of the pattern, since the standard fixes the spelling. Keep strptime for the text whose format you do not choose.
The %B pattern returns the month name in the language configured on the machine running the code. The same line therefore shows "juillet" on a French workstation and "July" on a server set to English. When the display language has to be a decision, format the month number and translate it yourself.
The time zone trap
What remains is what a datetime object does not necessarily say: where in the world its clock reading was taken. This is the most important distinction in the module, and the only one that produces bugs you cannot reproduce on your own machine. The naive shape says half past nine without saying half past nine where; the aware shape also carries an offset, therefore a single moment on the planet. The now method returns the first by default.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
naive = datetime.now() # a clock reading, unknown zone
moment = datetime.now(timezone.utc) # a moment, with no ambiguity
display = moment.astimezone(ZoneInfo("Europe/Paris")) # the same moment, seen from ParisGood practice sits in those three lines: compute and store in UTC, convert only at the last moment, for the screen. The conversion does not move the moment itself, it only changes the point of view.
Comparing or subtracting a naive shape and an aware one raises a TypeError that mentions offset-naive and offset-aware values: Python refuses to guess the missing zone. The textbook case is a date read back from the database, aware, meeting a local clock reading produced by the program. The utcnow method returned precisely a naive shape displaying UTC, and it has been deprecated since Python 3.12.
When it is not enough
A duration in months does not exist in the module, and that is no oversight: timedelta counts days and seconds, because "31 January plus one month" has no universal answer. Adding thirty days produces a wrong due date half the year, where the dateutil library and its relativedelta settle it according to an explicit convention.
Reading back a date written freely by a human is no more a job for strptime, nor for a regular expression stretched with every new case: the module expects a pattern known in advance. Measuring an execution time belongs to time.perf_counter, the wall clock being able to step backwards after a network resynchronisation and make a duration come out negative. And for a whole column of dates, pandas brings its own types, cut for volume.
Frequently asked questions
Why does everyone write from datetime import datetime?
Because the module and the main class it contains carry the same name. A plain import then forces the datetime.datetime.now() spelling, a redundancy the from form removes by importing the wanted class directly. The line only says this: from the datetime module, take the datetime object.
How do you add one month to a date?
By choosing the rule first, because none of them fits every case: 31 January plus one month is 28 February for a billing schedule, which must not spill over into March, and 3 March for a day counter. Once the convention is settled, it is written with the replace method on the month, or handed over to relativedelta, which applies the former.
Should dates be stored in UTC or in local time?
In UTC, with the zone attached, converting only at display time. A local reading saved without an offset becomes undecodable on the night the clocks go back, where half past two happens twice with nothing to tell the two apart. The ISO 8601 format produced by isoformat also happens to be what a JSON exchange expects. These reflexes are built by shipping a real project, which is what the Python course is for.