← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:timezone-utc into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:timezone-utc into launchpad:master.

Commit message:
Switch from pytz.UTC to datetime.timezone.utc

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/438679

Since Jürgen asked so nicely. :-)

Much of this was automatic, aided by `isort`, `black`, and `blackdoc`, although there were enough different spellings of things that I had to make a fair number of manual adjustments.  I tried to normalize the handling of `datetime` imports a bit along the way.

There are still a few remaining uses of `pytz` after this, but they're for non-UTC timezones and will generally need a bit more careful thought.
-- 
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:timezone-utc into launchpad:master.
diff --git a/lib/lp/answers/doc/expiration.rst b/lib/lp/answers/doc/expiration.rst
index 72d5087..3ef2ab2 100644
--- a/lib/lp/answers/doc/expiration.rst
+++ b/lib/lp/answers/doc/expiration.rst
@@ -33,10 +33,9 @@ somebody are subject to expiration.
     # By default, all open and needs info question should expire. Make
     # sure that no new questions were recently added and will make this
     # test fails in the future.
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from storm.locals import Or
-    >>> interval = datetime.now(pytz.UTC) - timedelta(days=15)
+    >>> interval = datetime.now(timezone.utc) - timedelta(days=15)
     >>> IStore(Question).find(
     ...     Question,
     ...     Or(
@@ -49,9 +48,7 @@ somebody are subject to expiration.
     # We need to massage sample data a little. Since all expiration
     # candidates in sample data would expire, do a little activity on
     # some of these.
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> now = datetime.now(timezone.utc)
     >>> two_weeks_ago = now - timedelta(days=14)
     >>> a_month_ago = now - timedelta(days=31)
     >>> from lp.services.webapp.interfaces import ILaunchBag
diff --git a/lib/lp/answers/doc/faqcollection.rst b/lib/lp/answers/doc/faqcollection.rst
index b5246dd..57d52e1 100644
--- a/lib/lp/answers/doc/faqcollection.rst
+++ b/lib/lp/answers/doc/faqcollection.rst
@@ -76,9 +76,8 @@ collection.
     ...     ),
     ... ]
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
 
     >>> faq_set = []
     >>> for owner, title, content, keywords in faq_specifications:
diff --git a/lib/lp/answers/doc/faqtarget.rst b/lib/lp/answers/doc/faqtarget.rst
index d02cdd9..8e6d1a0 100644
--- a/lib/lp/answers/doc/faqtarget.rst
+++ b/lib/lp/answers/doc/faqtarget.rst
@@ -82,9 +82,8 @@ accepts an optional date_created attribute (which defaults to the
 current time), and an optional keywords parameter used to initialize the
 FAQ's keywords.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> from datetime import datetime, timezone
+    >>> now = datetime.now(timezone.utc)
 
     >>> faq = target.newFAQ(
     ...     no_priv,
diff --git a/lib/lp/answers/doc/karma.rst b/lib/lp/answers/doc/karma.rst
index c38e049..5bc61ef 100644
--- a/lib/lp/answers/doc/karma.rst
+++ b/lib/lp/answers/doc/karma.rst
@@ -49,15 +49,14 @@ Setup an event listener to help ensure karma is assigned when it should.
 Define a generator that always give a date higher than the previous one
 to order our messages.
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timedelta, timezone
     >>> def timegenerator(origin):
     ...     now = origin
     ...     while True:
     ...         now += timedelta(seconds=5)
     ...         yield now
     ...
-    >>> now = timegenerator(datetime.now(UTC))
+    >>> now = timegenerator(datetime.now(timezone.utc))
 
 
 Karma Actions
diff --git a/lib/lp/answers/doc/question.rst b/lib/lp/answers/doc/question.rst
index fe69882..8f4e28c 100644
--- a/lib/lp/answers/doc/question.rst
+++ b/lib/lp/answers/doc/question.rst
@@ -112,9 +112,8 @@ The question status is 'Open'.
 
 The question has a creation time.
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> now - firefox_question.datecreated < timedelta(seconds=5)
     True
 
diff --git a/lib/lp/answers/doc/questionsets.rst b/lib/lp/answers/doc/questionsets.rst
index 79e43ad..d2eb057 100644
--- a/lib/lp/answers/doc/questionsets.rst
+++ b/lib/lp/answers/doc/questionsets.rst
@@ -236,13 +236,12 @@ Then some recent questions are created on a number of projects.
 
 A question is created just before the time limit on Launchpad.
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timedelta, timezone
     >>> question = launchpad.newQuestion(
     ...     no_priv,
     ...     "Launchpad question",
     ...     "A question",
-    ...     datecreated=datetime.now(UTC) - timedelta(days=61),
+    ...     datecreated=datetime.now(timezone.utc) - timedelta(days=61),
     ... )
     >>> login(ANONYMOUS)
 
diff --git a/lib/lp/answers/doc/questiontarget.rst b/lib/lp/answers/doc/questiontarget.rst
index 7004d2e..4c64219 100644
--- a/lib/lp/answers/doc/questiontarget.rst
+++ b/lib/lp/answers/doc/questiontarget.rst
@@ -39,9 +39,8 @@ description.  It also takes an optional parameter 'datecreated' which defaults
 to UTC_NOW.
 
     # Initialize 'now' to a known value.
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
 
     >>> question = target.newQuestion(
     ...     sample_person,
@@ -588,12 +587,10 @@ question.  The question's owner is the same as the bug's owner.  The question
 title and description are taken from the bug.  The comments on the bug are
 copied to the question.
 
-    >>> from datetime import datetime
     >>> from lp.bugs.interfaces.bug import CreateBugParams
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> from pytz import UTC
 
-    >>> now = datetime.now(UTC)
+    >>> now = datetime.now(timezone.utc)
     >>> target = getUtility(IProductSet)["jokosher"]
     >>> bug_params = CreateBugParams(
     ...     title="Print is broken",
diff --git a/lib/lp/answers/doc/workflow.rst b/lib/lp/answers/doc/workflow.rst
index b5ce3af..770d60b 100644
--- a/lib/lp/answers/doc/workflow.rst
+++ b/lib/lp/answers/doc/workflow.rst
@@ -65,9 +65,8 @@ Ubuntu Team owning the distribution.
 
 A question starts its lifecycle in the Open state.
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
-    >>> now = datetime.now(UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> new_question_args = dict(
     ...     owner=no_priv,
     ...     title="Unable to boot installer",
diff --git a/lib/lp/answers/model/faq.py b/lib/lp/answers/model/faq.py
index 5ce44c3..5ad759b 100644
--- a/lib/lp/answers/model/faq.py
+++ b/lib/lp/answers/model/faq.py
@@ -9,7 +9,8 @@ __all__ = [
     "FAQSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.expr import And, Desc
 from storm.properties import DateTime, Int, Unicode
@@ -52,7 +53,9 @@ class FAQ(StormBase):
 
     content = Unicode(allow_none=True, default=None)
 
-    date_created = DateTime(allow_none=False, default=DEFAULT, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=DEFAULT, tzinfo=timezone.utc
+    )
 
     last_updated_by_id = Int(
         name="last_updated_by",
@@ -63,7 +66,7 @@ class FAQ(StormBase):
     last_updated_by = Reference(last_updated_by_id, "Person.id")
 
     date_last_updated = DateTime(
-        allow_none=True, default=None, tzinfo=pytz.UTC
+        allow_none=True, default=None, tzinfo=timezone.utc
     )
 
     product_id = Int(name="product", allow_none=True, default=None)
diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
index a0089ee..3704ac3 100644
--- a/lib/lp/answers/model/question.py
+++ b/lib/lp/answers/model/question.py
@@ -13,10 +13,9 @@ __all__ = [
 ]
 
 import operator
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from email.utils import make_msgid
 
-import pytz
 from lazr.enum import DBItem, Item
 from lazr.lifecycle.event import ObjectCreatedEvent, ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
@@ -199,13 +198,17 @@ class Question(StormBase, BugLinkTargetMixin):
     answerer = Reference(answerer_id, "Person.id")
     answer_id = Int(name="answer", allow_none=True, default=None)
     answer = Reference(answer_id, "QuestionMessage.id")
-    datecreated = DateTime(allow_none=False, default=DEFAULT, tzinfo=pytz.UTC)
-    datedue = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    datecreated = DateTime(
+        allow_none=False, default=DEFAULT, tzinfo=timezone.utc
+    )
+    datedue = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
     datelastquery = DateTime(
-        allow_none=False, default=DEFAULT, tzinfo=pytz.UTC
+        allow_none=False, default=DEFAULT, tzinfo=timezone.utc
+    )
+    datelastresponse = DateTime(
+        allow_none=True, default=None, tzinfo=timezone.utc
     )
-    datelastresponse = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
-    date_solved = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_solved = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
     product_id = Int(name="product", allow_none=True, default=None)
     product = Reference(product_id, "Product.id")
     distribution_id = Int(name="distribution", allow_none=True, default=None)
@@ -853,7 +856,7 @@ class QuestionSet:
                 ),
             ),
         ]
-        expiry = datetime.now(pytz.UTC) - timedelta(
+        expiry = datetime.now(timezone.utc) - timedelta(
             days=days_before_expiration
         )
         return (
@@ -906,7 +909,7 @@ class QuestionSet:
         from lp.registry.model.distribution import Distribution
         from lp.registry.model.product import Product
 
-        time_cutoff = datetime.now(pytz.UTC) - timedelta(days=60)
+        time_cutoff = datetime.now(timezone.utc) - timedelta(days=60)
         question_count = Alias(Count())
 
         origin = (
@@ -1569,7 +1572,7 @@ class QuestionTargetMixin:
         )
         # Give the datelastresponse a current datetime, otherwise the
         # Launchpad Janitor would quickly expire questions made from old bugs.
-        question.datelastresponse = datetime.now(pytz.UTC)
+        question.datelastresponse = datetime.now(timezone.utc)
         # Directly create the BugLink so that users do not receive duplicate
         # messages about the bug.
         question.createBugLink(bug)
diff --git a/lib/lp/answers/model/questionreopening.py b/lib/lp/answers/model/questionreopening.py
index 0f68b75..7b850a2 100644
--- a/lib/lp/answers/model/questionreopening.py
+++ b/lib/lp/answers/model/questionreopening.py
@@ -5,7 +5,8 @@
 
 __all__ = ["QuestionReopening", "create_questionreopening"]
 
-import pytz
+from datetime import timezone
+
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.locals import DateTime, Int, Reference
 from zope.event import notify
@@ -31,7 +32,10 @@ class QuestionReopening(StormBase):
     question_id = Int(name="question", allow_none=False)
     question = Reference(question_id, "Question.id")
     datecreated = DateTime(
-        name="datecreated", allow_none=False, default=DEFAULT, tzinfo=pytz.UTC
+        name="datecreated",
+        allow_none=False,
+        default=DEFAULT,
+        tzinfo=timezone.utc,
     )
     reopener_id = Int(
         name="reopener", allow_none=False, validator=validate_public_person
@@ -44,7 +48,7 @@ class QuestionReopening(StormBase):
         validator=validate_public_person,
     )
     answerer = Reference(answerer_id, "Person.id")
-    date_solved = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_solved = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
     priorstate = DBEnum(
         name="priorstate", enum=QuestionStatus, allow_none=False
     )
diff --git a/lib/lp/answers/model/questionsubscription.py b/lib/lp/answers/model/questionsubscription.py
index 3abce7d..ba1c5da 100644
--- a/lib/lp/answers/model/questionsubscription.py
+++ b/lib/lp/answers/model/questionsubscription.py
@@ -5,7 +5,8 @@
 
 __all__ = ["QuestionSubscription"]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference
 from zope.interface import implementer
 
@@ -31,7 +32,9 @@ class QuestionSubscription(StormBase):
     )
     person = Reference(person_id, "Person.id")
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     def __init__(self, question, person):
         self.question = question
diff --git a/lib/lp/answers/model/tests/test_question.py b/lib/lp/answers/model/tests/test_question.py
index 62ea6c9..8b3abf4 100644
--- a/lib/lp/answers/model/tests/test_question.py
+++ b/lib/lp/answers/model/tests/test_question.py
@@ -1,9 +1,8 @@
 # Copyright 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -115,7 +114,7 @@ class TestQuestionSet(TestCaseWithFactory):
     def test_expiredQuestions(self):
         question = self.factory.makeQuestion()
         removeSecurityProxy(question).datelastquery = datetime.now(
-            pytz.UTC
+            timezone.utc
         ) - timedelta(days=5)
 
         question_set = getUtility(IQuestionSet)
diff --git a/lib/lp/answers/tests/emailinterface.rst b/lib/lp/answers/tests/emailinterface.rst
index 311f696..fa182bc 100644
--- a/lib/lp/answers/tests/emailinterface.rst
+++ b/lib/lp/answers/tests/emailinterface.rst
@@ -16,8 +16,7 @@ AnswerTrackerHandler.
     # have microseconds resolution. This means that it would be possible
     # for a message created using the DB API before one created by
     # the email interface to sort after.
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timedelta, timezone
     >>> def now_generator(now_ref):
     ...     now = now_ref
     ...     while True:
@@ -27,7 +26,7 @@ AnswerTrackerHandler.
 
     # We are using a date in the past because MessageSet disallows the
     # creation of email message with a future date.
-    >>> now = now_generator(datetime.now(UTC) - timedelta(hours=24))
+    >>> now = now_generator(datetime.now(timezone.utc) - timedelta(hours=24))
 
     # Define a helper function to send email to the Answer Tracker handler.
     >>> from lp.answers.mail.handler import AnswerTrackerHandler
diff --git a/lib/lp/answers/tests/test_question_webservice.py b/lib/lp/answers/tests/test_question_webservice.py
index 1d92cc7..a5735b2 100644
--- a/lib/lp/answers/tests/test_question_webservice.py
+++ b/lib/lp/answers/tests/test_question_webservice.py
@@ -4,9 +4,8 @@
 """Webservice unit tests related to Launchpad Questions."""
 
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from testtools.matchers import EndsWith
 
 from lp.answers.enums import QuestionStatus
@@ -331,7 +330,7 @@ class TestQuestionSetWebService(TestCaseWithFactory):
 
     def test_searchQuestions(self):
         date_gen = time_counter(
-            datetime(2015, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+            datetime(2015, 1, 1, tzinfo=timezone.utc), timedelta(days=1)
         )
         created = [
             self.factory.makeQuestion(title="foo", datecreated=next(date_gen))
diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py
index 18dbc10..733e24e 100644
--- a/lib/lp/answers/tests/test_question_workflow.py
+++ b/lib/lp/answers/tests/test_question_workflow.py
@@ -10,11 +10,10 @@ than necessary. This is tested here.
 """
 
 import traceback
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import List
 
 from lazr.lifecycle.interfaces import IObjectCreatedEvent, IObjectModifiedEvent
-from pytz import UTC
 from zope.component import getUtility
 from zope.interface.verify import verifyObject
 from zope.security.interfaces import Unauthorized
@@ -53,7 +52,7 @@ class BaseAnswerTrackerWorkflowTestCase(TestCase):
     def setUp(self):
         super().setUp()
 
-        self.now = datetime.now(UTC)
+        self.now = datetime.now(timezone.utc)
 
         # Login as the question owner.
         login("no-priv@xxxxxxxxxxxxx")
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index 0377cff..0e0b0e5 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -7,7 +7,7 @@ import math
 import os.path
 import sys
 from bisect import bisect
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from email.utils import formatdate, mktime_tz
 from textwrap import dedent
 from urllib.parse import quote
@@ -2379,7 +2379,7 @@ class DateTimeFormatterAPI:
                 datetimeobject.year,
                 datetimeobject.month,
                 datetimeobject.day,
-                tzinfo=pytz.timezone("UTC"),
+                tzinfo=timezone.utc,
             )
 
     def time(self):
@@ -2399,7 +2399,7 @@ class DateTimeFormatterAPI:
         # This method exists to be overridden in tests.
         if self._datetime.tzinfo:
             # datetime is offset-aware
-            return datetime.now(pytz.timezone("UTC"))
+            return datetime.now(timezone.utc)
         else:
             # datetime is offset-naive
             return datetime.utcnow()
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 4b81a7e..c827743 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -4,11 +4,10 @@
 """Test vocabulary adapters."""
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 from typing import List
 from urllib.parse import urlencode
 
-import pytz
 from zope.component import getSiteManager, getUtility
 from zope.formlib.interfaces import MissingInputError
 from zope.interface import implementer
@@ -132,7 +131,7 @@ class PersonPickerEntrySourceAdapterTestCase(TestCaseWithFactory):
     def test_PersonPickerEntrySourceAdapter_user(self):
         # The person picker provides more information for users.
         person = self.factory.makePerson(email="snarf@xxxxxx", name="snarf")
-        creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone("UTC"))
+        creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, timezone.utc)
         removeSecurityProxy(person).datecreated = creation_date
         getUtility(IIrcIDSet).new(person, "eg.dom", "snarf")
         getUtility(IIrcIDSet).new(person, "ex.dom", "pting")
@@ -343,7 +342,7 @@ class TestProductPickerEntrySourceAdapter(TestCaseWithFactory):
     def test_provides_commercial_subscription_expired(self):
         product = self.factory.makeProduct(name="fnord")
         self.factory.makeCommercialSubscription(product)
-        then = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
+        then = datetime(2005, 6, 15, 0, 0, 0, 0, timezone.utc)
         with celebrity_logged_in("admin"):
             product.commercial_subscription.date_expires = then
         self.assertEqual(
@@ -497,7 +496,7 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
             distribution=distribution, status=SeriesStatus.CURRENT
         )
         self.factory.makeCommercialSubscription(distribution)
-        then = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
+        then = datetime(2005, 6, 15, 0, 0, 0, 0, timezone.utc)
         with celebrity_logged_in("admin"):
             distribution.commercial_subscription.date_expires = then
         self.assertEqual(
@@ -602,7 +601,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
             membership_policy=TeamMembershipPolicy.RESTRICTED,
         )
         person = self.factory.makePerson(name="xpting-person")
-        creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone("UTC"))
+        creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, timezone.utc)
         removeSecurityProxy(person).datecreated = creation_date
         TestPersonVocabulary.test_persons.extend([team, person])
         product = self.factory.makeProduct(owner=team)
diff --git a/lib/lp/app/doc/displaying-dates.rst b/lib/lp/app/doc/displaying-dates.rst
index 401e522..de75cf9 100644
--- a/lib/lp/app/doc/displaying-dates.rst
+++ b/lib/lp/app/doc/displaying-dates.rst
@@ -26,10 +26,8 @@ hence if the display will be the date.
 
 First, let's bring in some dependencies:
 
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.testing import test_tales
-    >>> import pytz
-    >>> UTC = pytz.timezone("UTC")
 
 fmt:approximatedate and fmt:displaydate display the difference between
 the formatted timestamp and the present.  This is a really bad idea
@@ -37,7 +35,7 @@ for tests, so we register an alternate formatter that use the same
 formatting code, but always display the difference from a known
 timestamp.
 
-    >>> fixed_time_utc = datetime(2005, 12, 25, 12, 0, 0, tzinfo=UTC)
+    >>> fixed_time_utc = datetime(2005, 12, 25, 12, 0, 0, tzinfo=timezone.utc)
     >>> fixed_time = datetime(2005, 12, 25, 12, 0, 0)
     >>> from lp.app.browser.tales import DateTimeFormatterAPI
     >>> class TestDateTimeFormatterAPI(DateTimeFormatterAPI):
diff --git a/lib/lp/app/tests/test_tales.py b/lib/lp/app/tests/test_tales.py
index 0230f4f..3fd6f32 100644
--- a/lib/lp/app/tests/test_tales.py
+++ b/lib/lp/app/tests/test_tales.py
@@ -3,10 +3,9 @@
 
 """tales.py doctests."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 from lxml import html
-from pytz import utc
 from zope.component import getAdapter, getUtility
 from zope.traversing.interfaces import IPathAdapter, TraversalError
 
@@ -418,8 +417,8 @@ class TestDateTimeFormatterAPI(TestCase):
         """Test that year delta gives reasonable values."""
 
         def assert_delta(expected, old, new):
-            old = datetime(*old, tzinfo=utc)
-            new = datetime(*new, tzinfo=utc)
+            old = datetime(*old, tzinfo=timezone.utc)
+            new = datetime(*new, tzinfo=timezone.utc)
             delta = DateTimeFormatterAPI._yearDelta(old, new)
             self.assertEqual(expected, delta)
 
@@ -431,7 +430,7 @@ class TestDateTimeFormatterAPI(TestCase):
 
     def getDurationsince(self, delta):
         """Return the durationsince for a given delta."""
-        creation = datetime(2000, 1, 1, tzinfo=utc)
+        creation = datetime(2000, 1, 1, tzinfo=timezone.utc)
         formatter = DateTimeFormatterAPI(creation)
         formatter._now = lambda: creation + delta
         return formatter.durationsince()
diff --git a/lib/lp/app/widgets/announcementdate.py b/lib/lp/app/widgets/announcementdate.py
index 0969253..c61adb4 100644
--- a/lib/lp/app/widgets/announcementdate.py
+++ b/lib/lp/app/widgets/announcementdate.py
@@ -1,9 +1,8 @@
 # Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.formlib import form
 from zope.formlib.interfaces import (
     ConversionError,
@@ -134,7 +133,7 @@ class AnnouncementDateWidget(SimpleInputWidget):
             )
             raise self._error
         if action == "immediately":
-            return datetime.now(pytz.utc)
+            return datetime.now(timezone.utc)
         elif action == "sometime":
             return None
         elif action == "specific":
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 259007d..186cf39 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -19,7 +19,6 @@ __all__ = [
 
 from datetime import datetime, timezone
 
-import pytz
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.datetime import DateTimeError, parse
@@ -78,9 +77,7 @@ class DateTimeWidget(TextWidget):
     time zone even if the date provided was in a different time zone.
 
       >>> widget.request.form[widget.name] = "2005-07-03"
-      >>> widget.from_date = datetime(
-      ...     2006, 5, 23, tzinfo=pytz.timezone("UTC")
-      ... )
+      >>> widget.from_date = datetime(2006, 5, 23, tzinfo=timezone.utc)
       >>> print(widget.getInputValue())
       ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
       Traceback (most recent call last):
@@ -97,7 +94,7 @@ class DateTimeWidget(TextWidget):
 
     If to_date is provided then getInputValue() will enforce this too.
 
-      >>> widget.to_date = datetime(2008, 1, 26, tzinfo=pytz.timezone("UTC"))
+      >>> widget.to_date = datetime(2008, 1, 26, tzinfo=timezone.utc)
       >>> print(widget.getInputValue())
       ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
       Traceback (most recent call last):
@@ -178,7 +175,7 @@ class DateTimeWidget(TextWidget):
         of that.
 
           >>> print(type(widget.time_zone))
-          <class 'pytz.UTC'>
+          <class 'timezone.utc'>
 
         The widget required_time_zone is None by default.
 
@@ -388,9 +385,9 @@ class DateTimeWidget(TextWidget):
           >>> from zope.schema import Field
           >>> field = Field(__name__="foo", title="Foo")
           >>> widget = DateTimeWidget(field, TestRequest())
-          >>> widget.required_time_zone = pytz.timezone("UTC")
+          >>> widget.required_time_zone = timezone.utc
           >>> widget.time_zone
-          <UTC>
+          datetime.timezone.utc
 
         The widget converts an empty string to the missing value:
 
@@ -447,7 +444,7 @@ class DateTimeWidget(TextWidget):
         DateTimes are displayed without the corresponding time zone
         information:
 
-          >>> dt = datetime(2006, 1, 1, 12, 0, 0, tzinfo=pytz.timezone("UTC"))
+          >>> dt = datetime(2006, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
           >>> widget._toFormValue(dt)
           '2006-01-01 12:00:00'
 
@@ -513,15 +510,15 @@ class DateWidget(DateTimeWidget):
     zone even if it is stored as a datetime.
 
       >>> widget.time_zone
-      <UTC>
+      datetime.timezone.utc
 
       >>> widget.system_time_zone = pytz.timezone("America/New_York")
       >>> widget.time_zone
-      <UTC>
+      datetime.timezone.utc
 
       >>> widget.required_time_zone = pytz.timezone("America/Los_Angeles")
       >>> widget.time_zone
-      <UTC>
+      datetime.timezone.utc
 
     A date picker can be disabled initially:
 
@@ -534,7 +531,7 @@ class DateWidget(DateTimeWidget):
     """
 
     timeformat = "%Y-%m-%d"
-    time_zone = pytz.timezone("UTC")
+    time_zone = timezone.utc
 
     # ZPT that renders our widget
     __call__ = ViewPageTemplateFile("templates/date.pt")
@@ -601,7 +598,7 @@ class DateWidget(DateTimeWidget):
         The widget ignores time and time zone information, returning only
         the date:
 
-          >>> dt = datetime(2006, 1, 1, 12, 0, 0, tzinfo=pytz.timezone("UTC"))
+          >>> dt = datetime(2006, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
           >>> widget._toFormValue(dt)
           '2006-01-01'
 
diff --git a/lib/lp/app/widgets/doc/announcement-date-widget.rst b/lib/lp/app/widgets/doc/announcement-date-widget.rst
index 6cb658b..4dd1b95 100644
--- a/lib/lp/app/widgets/doc/announcement-date-widget.rst
+++ b/lib/lp/app/widgets/doc/announcement-date-widget.rst
@@ -31,11 +31,10 @@ return the date you specified.
 When you choose to publish immediately, the widget will return the current
 date and time.
 
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> action_widget.request.form[action_widget.name] = "immediately"
     >>> date_widget.request.form[date_widget.name] = ""
-    >>> now = datetime.now(pytz.utc)
+    >>> now = datetime.now(timezone.utc)
     >>> before = now - timedelta(1)  # 1 day
     >>> after = now + timedelta(1)  # 1 day
     >>> immediate_date = widget.getInputValue()
diff --git a/lib/lp/app/widgets/suggestion.py b/lib/lp/app/widgets/suggestion.py
index 777585f..69cf947 100644
--- a/lib/lp/app/widgets/suggestion.py
+++ b/lib/lp/app/widgets/suggestion.py
@@ -10,9 +10,8 @@ __all__ = [
 ]
 
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from zope.component import getMultiAdapter, getUtility
 from zope.formlib.interfaces import IInputWidget, InputErrors
 from zope.formlib.utility import setUpWidget
@@ -232,7 +231,7 @@ class TargetBranchWidget(SuggestionWidget):
         """
         default_target = branch.target.default_merge_target
         logged_in_user = getUtility(ILaunchBag).user
-        since = datetime.now(utc) - timedelta(days=90)
+        since = datetime.now(timezone.utc) - timedelta(days=90)
         collection = branch.target.collection.targetedBy(logged_in_user, since)
         collection = collection.visibleByUser(logged_in_user)
         # May actually need some eager loading, but the API isn't fine grained
@@ -321,7 +320,7 @@ class TargetGitRepositoryWidget(SuggestionWidget):
                 repository.target
             )
             logged_in_user = getUtility(ILaunchBag).user
-            since = datetime.now(utc) - timedelta(days=90)
+            since = datetime.now(timezone.utc) - timedelta(days=90)
             collection = IGitCollection(repository.target).targetedBy(
                 logged_in_user, since
             )
diff --git a/lib/lp/app/widgets/tests/test_suggestion.py b/lib/lp/app/widgets/tests/test_suggestion.py
index 63d87c5..36c1a9b 100644
--- a/lib/lp/app/widgets/tests/test_suggestion.py
+++ b/lib/lp/app/widgets/tests/test_suggestion.py
@@ -2,9 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import doctest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from testtools.matchers import DocTestMatches
 from zope.component import getUtility, provideUtility
 from zope.interface import implementer
@@ -147,7 +146,7 @@ class TestTargetBranchWidget(TestCaseWithFactory):
     def makeBranchAndOldMergeProposal(self, timedelta):
         """Make an old merge proposal and a branch with the same target."""
         bmp = self.factory.makeBranchMergeProposal(
-            date_created=datetime.now(utc) - timedelta
+            date_created=datetime.now(timezone.utc) - timedelta
         )
         login_person(bmp.registrant)
         target = bmp.target_branch
@@ -201,7 +200,7 @@ class TestTargetGitRepositoryWidget(TestCaseWithFactory):
     def makeRefAndOldMergeProposal(self, timedelta):
         """Make an old merge proposal and a ref with the same target."""
         bmp = self.factory.makeBranchMergeProposalForGit(
-            date_created=datetime.now(utc) - timedelta
+            date_created=datetime.now(timezone.utc) - timedelta
         )
         login_person(bmp.registrant)
         target = bmp.merge_target
@@ -230,7 +229,7 @@ class TestTargetGitRepositoryWidget(TestCaseWithFactory):
         bmp = self.factory.makeBranchMergeProposalForGit(
             source_ref=this_source,
             target_ref=this_target,
-            date_created=datetime.now(utc) - timedelta(days=1),
+            date_created=datetime.now(timezone.utc) - timedelta(days=1),
         )
         other_source, other_target = self.factory.makeGitRefs(
             owner=owner,
@@ -240,7 +239,7 @@ class TestTargetGitRepositoryWidget(TestCaseWithFactory):
         self.factory.makeBranchMergeProposalForGit(
             source_ref=other_source,
             target_ref=other_target,
-            date_created=datetime.now(utc) - timedelta(days=1),
+            date_created=datetime.now(timezone.utc) - timedelta(days=1),
         )
         login_person(bmp.registrant)
         [source] = self.factory.makeGitRefs(repository=this_target.repository)
@@ -278,7 +277,7 @@ class TestTargetGitRepositoryWidget(TestCaseWithFactory):
         bmp = self.factory.makeBranchMergeProposalForGit(
             source_ref=source,
             target_ref=target,
-            date_created=datetime.now(utc) - timedelta(days=1),
+            date_created=datetime.now(timezone.utc) - timedelta(days=1),
         )
         login_person(bmp.registrant)
         widget = make_target_git_repository_widget(source)
diff --git a/lib/lp/archivepublisher/deathrow.py b/lib/lp/archivepublisher/deathrow.py
index 80f463e..cca0368 100644
--- a/lib/lp/archivepublisher/deathrow.py
+++ b/lib/lp/archivepublisher/deathrow.py
@@ -5,9 +5,8 @@
 Processes removals of packages that are scheduled for deletion.
 """
 
-import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.expr import Exists
 from storm.locals import And, ClassAlias, Not, Select
 
@@ -208,7 +207,7 @@ class DeathRow:
             publication_class, *clauses
         )
 
-        right_now = datetime.datetime.now(pytz.timezone("UTC"))
+        right_now = datetime.now(timezone.utc)
         for pub in all_publications:
             # Deny removal if any reference is still active.
             if pub.status not in inactive_publishing_status:
diff --git a/lib/lp/archivepublisher/scripts/publish_ftpmaster.py b/lib/lp/archivepublisher/scripts/publish_ftpmaster.py
index ab301a6..2054e6e 100644
--- a/lib/lp/archivepublisher/scripts/publish_ftpmaster.py
+++ b/lib/lp/archivepublisher/scripts/publish_ftpmaster.py
@@ -10,9 +10,8 @@ __all__ = [
 import math
 import os
 import shutil
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import utc
 from zope.component import getUtility
 
 from lp.archivepublisher.config import getPubConfig
@@ -256,7 +255,7 @@ class PublishFTPMaster(LaunchpadCronScript):
         with open(marker_name, "w") as marker:
             marker.write(
                 "Indexes for %s were created on %s.\n"
-                % (suite, datetime.now(utc))
+                % (suite, datetime.now(timezone.utc))
             )
 
     def createIndexes(self, distribution, suites):
diff --git a/lib/lp/archivepublisher/scripts/sync_signingkeys.py b/lib/lp/archivepublisher/scripts/sync_signingkeys.py
index 7c59734..7bb200a 100644
--- a/lib/lp/archivepublisher/scripts/sync_signingkeys.py
+++ b/lib/lp/archivepublisher/scripts/sync_signingkeys.py
@@ -8,10 +8,9 @@ __all__ = [
 ]
 
 import os
-from datetime import datetime
+from datetime import datetime, timezone
 
 import transaction
-from pytz import utc
 from storm.locals import Store
 from zope.component import getUtility
 
@@ -226,7 +225,7 @@ class SyncSigningKeysScript(LaunchpadScript):
             with open(pub_key_path, "rb") as fd:
                 public_key = fd.read()
 
-            now = datetime.now().replace(tzinfo=utc)
+            now = datetime.now().replace(tzinfo=timezone.utc)
             description = "%s key for %s" % (key_type.name, archive.reference)
             return arch_signing_key_set.inject(
                 key_type,
@@ -267,7 +266,7 @@ class SyncSigningKeysScript(LaunchpadScript):
             )
         else:
             public_key = gpg_handler.retrieveKey(secret_key.fingerprint)
-            now = datetime.now().replace(tzinfo=utc)
+            now = datetime.now().replace(tzinfo=timezone.utc)
             return signing_key_set.inject(
                 SigningKeyType.OPENPGP,
                 secret_key.export(),
diff --git a/lib/lp/archivepublisher/signing.py b/lib/lp/archivepublisher/signing.py
index d874938..cad47f1 100644
--- a/lib/lp/archivepublisher/signing.py
+++ b/lib/lp/archivepublisher/signing.py
@@ -21,10 +21,9 @@ import subprocess
 import tarfile
 import tempfile
 import textwrap
-from datetime import datetime
+from datetime import datetime, timezone
 from functools import partial
 
-from pytz import utc
 from zope.component import getUtility
 
 from lp.archivepublisher.config import getPubConfig
@@ -510,7 +509,7 @@ class SigningUpload(CustomUpload):
         with open(public_key_file, "rb") as fd:
             public_key = fd.read()
 
-        now = datetime.now().replace(tzinfo=utc)
+        now = datetime.now().replace(tzinfo=timezone.utc)
         description = "%s key for %s" % (key_type.name, self.archive.reference)
         key_set.inject(
             key_type,
diff --git a/lib/lp/archivepublisher/tests/deathrow.rst b/lib/lp/archivepublisher/tests/deathrow.rst
index bd5c1bf..cbe463f 100644
--- a/lib/lp/archivepublisher/tests/deathrow.rst
+++ b/lib/lp/archivepublisher/tests/deathrow.rst
@@ -49,17 +49,15 @@ Setup `SoyuzTestPublisher` for creating publications for Ubuntu/hoary.
 Build a 'past' and a 'future' timestamps to be used as
 'scheduleddeletiondate'.
 
-    >>> import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
 
-    >>> UTC = pytz.timezone("UTC")
-    >>> this_year = datetime.datetime.now().year
+    >>> this_year = datetime.now().year
 
-    >>> past_date = datetime.datetime(
-    ...     year=this_year - 2, month=1, day=1, tzinfo=UTC
+    >>> past_date = datetime(
+    ...     year=this_year - 2, month=1, day=1, tzinfo=timezone.utc
     ... )
-    >>> future_date = datetime.datetime(
-    ...     year=this_year + 2, month=1, day=1, tzinfo=UTC
+    >>> future_date = datetime(
+    ...     year=this_year + 2, month=1, day=1, tzinfo=timezone.utc
     ... )
 
 Create source publications in various statuses that are all ready to
diff --git a/lib/lp/archivepublisher/tests/test_processdeathrow.py b/lib/lp/archivepublisher/tests/test_processdeathrow.py
index 1b0e0b1..33ca946 100644
--- a/lib/lp/archivepublisher/tests/test_processdeathrow.py
+++ b/lib/lp/archivepublisher/tests/test_processdeathrow.py
@@ -8,13 +8,12 @@ of the module functionality; here we just aim to test that the script
 processes its arguments and handles dry-run correctly.
 """
 
-import datetime
 import os
 import shutil
 import subprocess
+from datetime import datetime, timezone
 from tempfile import mkdtemp
 
-import pytz
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -148,8 +147,8 @@ class TestProcessDeathRow(TestCaseWithFactory):
         for pubrec in pubrecs:
             pubrec.status = PackagePublishingStatus.SUPERSEDED
             pubrec.dateremoved = None
-            pubrec.scheduleddeletiondate = datetime.datetime(
-                1999, 1, 1, tzinfo=pytz.UTC
+            pubrec.scheduleddeletiondate = datetime(
+                1999, 1, 1, tzinfo=timezone.utc
             )
             pubrec_ids.append(pubrec.id)
         return pubrec_ids
@@ -169,7 +168,7 @@ class TestProcessDeathRow(TestCaseWithFactory):
     def probeRemoved(self, pubrec_ids):
         """Check if all source publishing records were removed."""
         store = IStore(SourcePackagePublishingHistory)
-        right_now = datetime.datetime.now(pytz.timezone("UTC"))
+        right_now = datetime.now(timezone.utc)
         for pubrec_id in pubrec_ids:
             spph = store.get(SourcePackagePublishingHistory, pubrec_id)
             self.assertTrue(
diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py
index 3aed43f..46556e3 100644
--- a/lib/lp/archivepublisher/tests/test_publisher.py
+++ b/lib/lp/archivepublisher/tests/test_publisher.py
@@ -13,14 +13,13 @@ import stat
 import tempfile
 import time
 from collections import OrderedDict, defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from fnmatch import fnmatch
 from functools import partial
 from textwrap import dedent
 from typing import Optional, Sequence, Tuple
 from unittest import mock
 
-import pytz
 import six
 import transaction
 from debian.deb822 import Release
@@ -1491,7 +1490,7 @@ class TestPublisher(TestPublisherBase):
         self.assertNotIn(archive, ubuntu.getPendingPublicationPPAs())
         archive_file = self.factory.makeArchiveFile(archive=archive)
         self.assertNotIn(archive, ubuntu.getPendingPublicationPPAs())
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         removeSecurityProxy(
             archive_file
         ).scheduled_deletion_date = now + timedelta(hours=12)
@@ -3017,7 +3016,7 @@ class TestUpdateByHash(TestPublisherBase):
 
     def setUpMockTime(self):
         """Start simulating the advance of time in the publisher."""
-        self.times = [datetime.now(pytz.UTC)]
+        self.times = [datetime.now(timezone.utc)]
         mock_datetime = mock.patch("lp.archivepublisher.publishing.datetime")
         mocked_datetime = mock_datetime.start()
         self.addCleanup(mock_datetime.stop)
diff --git a/lib/lp/archivepublisher/tests/test_signing.py b/lib/lp/archivepublisher/tests/test_signing.py
index e8a3fcc..696053d 100644
--- a/lib/lp/archivepublisher/tests/test_signing.py
+++ b/lib/lp/archivepublisher/tests/test_signing.py
@@ -8,11 +8,10 @@ import re
 import shutil
 import stat
 import tarfile
-from datetime import datetime
+from datetime import datetime, timezone
 from unittest.mock import call
 
 from fixtures import MockPatch, MonkeyPatch
-from pytz import utc
 from testtools.matchers import (
     Contains,
     Equals,
@@ -2639,7 +2638,7 @@ class TestSigningUploadWithSigningService(TestSigningHelpers):
                 private_key,
                 public_key,
                 "OPAL key for %s" % self.archive.reference,
-                now.replace(tzinfo=utc),
+                now.replace(tzinfo=timezone.utc),
             ),
             self.signing_service_client.inject.call_args[0],
         )
diff --git a/lib/lp/archivepublisher/tests/test_sync_signingkeys.py b/lib/lp/archivepublisher/tests/test_sync_signingkeys.py
index 63f39ec..05a0d24 100644
--- a/lib/lp/archivepublisher/tests/test_sync_signingkeys.py
+++ b/lib/lp/archivepublisher/tests/test_sync_signingkeys.py
@@ -8,13 +8,12 @@ __all__ = [
 ]
 
 import os
-from datetime import datetime
+from datetime import datetime, timezone
 from textwrap import dedent
 from unittest import mock
 
 import transaction
 from fixtures import MockPatch, TempDir
-from pytz import utc
 from testtools.content import text_content
 from testtools.matchers import (
     ContainsAll,
@@ -587,7 +586,7 @@ class TestSyncSigningKeysScript(TestCaseWithFactory):
                 b"Private key content",
                 b"Public key content",
                 "UEFI key for %s" % archive.reference,
-                now.replace(tzinfo=utc),
+                now.replace(tzinfo=timezone.utc),
             ),
             signing_service_client.inject.call_args[0],
         )
@@ -620,7 +619,7 @@ class TestSyncSigningKeysScript(TestCaseWithFactory):
                 b"Private key content",
                 b"Public key content",
                 "UEFI key for %s" % archive.reference,
-                now.replace(tzinfo=utc),
+                now.replace(tzinfo=timezone.utc),
             ),
             signing_service_client.inject.call_args[0],
         )
@@ -716,7 +715,7 @@ class TestSyncSigningKeysScript(TestCaseWithFactory):
                     b"Private key content",
                     b"Public key content",
                     "UEFI key for %s" % archive.reference,
-                    now.replace(tzinfo=utc),
+                    now.replace(tzinfo=timezone.utc),
                 )
             ],
             signing_service_client.inject.call_args,
@@ -765,7 +764,7 @@ class TestSyncSigningKeysScript(TestCaseWithFactory):
                 public_key=StartsWith(
                     b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
                 ),
-                date_created=Equals(now.replace(tzinfo=utc)),
+                date_created=Equals(now.replace(tzinfo=timezone.utc)),
             ),
         )
         with open(secret_key_path, "rb") as f:
@@ -779,7 +778,7 @@ class TestSyncSigningKeysScript(TestCaseWithFactory):
                     Equals(secret_key_bytes),
                     StartsWith(b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"),
                     Equals("Launchpad PPA for Celso áéíóú Providelo"),
-                    Equals(now.replace(tzinfo=utc)),
+                    Equals(now.replace(tzinfo=timezone.utc)),
                 ]
             ),
         )
diff --git a/lib/lp/archiveuploader/uploadprocessor.py b/lib/lp/archiveuploader/uploadprocessor.py
index 73bc9e1..bb7c326 100644
--- a/lib/lp/archiveuploader/uploadprocessor.py
+++ b/lib/lp/archiveuploader/uploadprocessor.py
@@ -48,9 +48,8 @@ above, failed being worst).
 import os
 import shutil
 import sys
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.component import getUtility
 
 from lp.app.errors import NotFoundError
@@ -224,12 +223,12 @@ class UploadProcessor:
                 set_request_started(enable_timeout=False)
                 try:
                     handler = UploadHandler.forProcessor(self, fsroot, upload)
-                    date_started = datetime.now(pytz.UTC)
+                    date_started = datetime.now(timezone.utc)
                 except CannotGetBuild as e:
                     self.log.warning(e)
                 else:
                     handler.process()
-                    date_completed = datetime.now(pytz.UTC)
+                    date_completed = datetime.now(timezone.utc)
                     upload_duration = (
                         date_completed - date_started
                     ).total_seconds() * 1000
diff --git a/lib/lp/blueprints/browser/tests/sprintattendance-views.rst b/lib/lp/blueprints/browser/tests/sprintattendance-views.rst
index 17c307d..8963cb6 100644
--- a/lib/lp/blueprints/browser/tests/sprintattendance-views.rst
+++ b/lib/lp/blueprints/browser/tests/sprintattendance-views.rst
@@ -48,10 +48,10 @@ set.
     []
 
     >>> ubz.time_starts
-    datetime.datetime(2005, 10, 7, 23, 30, tzinfo=<UTC>)
+    datetime.datetime(2005, 10, 7, 23, 30, tzinfo=datetime.timezone.utc)
 
     >>> ubz.time_ends
-    datetime.datetime(2005, 11, 17, 0, 11, tzinfo=<UTC>)
+    datetime.datetime(2005, 11, 17, 0, 11, tzinfo=datetime.timezone.utc)
 
     >>> login("test@xxxxxxxxxxxxx")
 
diff --git a/lib/lp/blueprints/browser/tests/test_specification.py b/lib/lp/blueprints/browser/tests/test_specification.py
index 1405a16..4dd927b 100644
--- a/lib/lp/blueprints/browser/tests/test_specification.py
+++ b/lib/lp/blueprints/browser/tests/test_specification.py
@@ -4,9 +4,8 @@
 import json
 import re
 import unittest
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import soupmatchers
 import transaction
 from fixtures import FakeLogger
@@ -931,7 +930,7 @@ class TestSpecificationFieldXHTMLRepresentations(TestCaseWithFactory):
     def test_starter_set(self):
         user = self.factory.makePerson()
         blueprint = self.factory.makeBlueprint(owner=user)
-        when = datetime(2011, 1, 1, tzinfo=pytz.UTC)
+        when = datetime(2011, 1, 1, tzinfo=timezone.utc)
         with person_logged_in(user):
             blueprint.setImplementationStatus(
                 SpecificationImplementationStatus.STARTED, user
@@ -953,7 +952,7 @@ class TestSpecificationFieldXHTMLRepresentations(TestCaseWithFactory):
     def test_completer_set(self):
         user = self.factory.makePerson()
         blueprint = self.factory.makeBlueprint(owner=user)
-        when = datetime(2011, 1, 1, tzinfo=pytz.UTC)
+        when = datetime(2011, 1, 1, tzinfo=timezone.utc)
         with person_logged_in(user):
             blueprint.setImplementationStatus(
                 SpecificationImplementationStatus.IMPLEMENTED, user
diff --git a/lib/lp/blueprints/doc/sprint-meeting-export.rst b/lib/lp/blueprints/doc/sprint-meeting-export.rst
index cffcf2a..e251b98 100644
--- a/lib/lp/blueprints/doc/sprint-meeting-export.rst
+++ b/lib/lp/blueprints/doc/sprint-meeting-export.rst
@@ -9,8 +9,7 @@ communicate this.
 
 First we import the classes required to test the view:
 
-    >>> from datetime import datetime
-    >>> from pytz import timezone
+    >>> from datetime import datetime, timezone
     >>> from zope.component import getUtility, getMultiAdapter
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> from lp.blueprints.browser.sprint import SprintMeetingExportView
@@ -85,8 +84,8 @@ assigned).
 This is because sample person has not registered as an attendee of the
 sprint.  If we add them as an attendee, then they will be available:
 
-    >>> time_starts = datetime(2005, 10, 8, 7, 0, 0, tzinfo=timezone("UTC"))
-    >>> time_ends = datetime(2005, 11, 17, 20, 0, 0, tzinfo=timezone("UTC"))
+    >>> time_starts = datetime(2005, 10, 8, 7, 0, 0, tzinfo=timezone.utc)
+    >>> time_ends = datetime(2005, 11, 17, 20, 0, 0, tzinfo=timezone.utc)
     >>> ignored = login_person(sampleperson)
     >>> ubz.attend(sampleperson, time_starts, time_ends, True)
     <...SprintAttendance ...>
diff --git a/lib/lp/blueprints/doc/sprint.rst b/lib/lp/blueprints/doc/sprint.rst
index 60e0783..c2fbce7 100644
--- a/lib/lp/blueprints/doc/sprint.rst
+++ b/lib/lp/blueprints/doc/sprint.rst
@@ -3,11 +3,7 @@ Sprints / Meetings
 
 Sprints or meetings can be coordinated using Launchpad.
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.component import getUtility
     >>> from lp.blueprints.interfaces.sprint import ISprintSet
     >>> from lp.registry.interfaces.person import IPersonSet
@@ -35,7 +31,7 @@ Make a new sprint and add some relevant specifications to it.
 
     >>> futurista = factory.makeSprint(
     ...     name="futurista",
-    ...     time_starts=datetime.now(pytz.UTC) + timedelta(days=1),
+    ...     time_starts=datetime.now(timezone.utc) + timedelta(days=1),
     ... )
     >>> firefox_spec = firefox.specifications(futurista.owner)[0]
     >>> _ = firefox_spec.linkSprint(futurista, futurista.owner)
@@ -250,8 +246,8 @@ Sprint attendance
 The sprint attend() method adds a user's attendance to a sprint.
 
     >>> person = factory.makePerson(name="mustard")
-    >>> time_starts = datetime(2005, 10, 7, 9, 0, 0, 0, pytz.UTC)
-    >>> time_ends = datetime(2005, 10, 17, 19, 5, 0, 0, pytz.UTC)
+    >>> time_starts = datetime(2005, 10, 7, 9, 0, 0, 0, timezone.utc)
+    >>> time_ends = datetime(2005, 10, 17, 19, 5, 0, 0, timezone.utc)
     >>> sprint_attendance = ubz.attend(person, time_starts, time_ends, True)
 
 The attend() method can update a user's attendance if there is already a
@@ -269,8 +265,8 @@ ISprintAttendance for the user.
     >>> print(sprint_attendance.is_physical)
     True
 
-    >>> time_starts = datetime(2005, 10, 8, 9, 0, 0, 0, pytz.UTC)
-    >>> time_ends = datetime(2005, 10, 16, 19, 5, 0, 0, pytz.UTC)
+    >>> time_starts = datetime(2005, 10, 8, 9, 0, 0, 0, timezone.utc)
+    >>> time_ends = datetime(2005, 10, 16, 19, 5, 0, 0, timezone.utc)
     >>> new_attendance = ubz.attend(person, time_starts, time_ends, False)
     >>> print(new_attendance.attendee.name)
     mustard
diff --git a/lib/lp/blueprints/doc/sprintattendance.rst b/lib/lp/blueprints/doc/sprintattendance.rst
index f3c5bcd..75133cb 100644
--- a/lib/lp/blueprints/doc/sprintattendance.rst
+++ b/lib/lp/blueprints/doc/sprintattendance.rst
@@ -5,15 +5,13 @@ The SprintAttendance object links a person to a sprint. It records additional
 information about the attendance. The start and end date-times are required
 and they must be UTC
 
-    >>> import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
     >>> from lp.blueprints.model.sprintattendance import SprintAttendance
 
     >>> sprint = factory.makeSprint(title="lunarbase")
     >>> person = factory.makePerson(name="scarlet")
-    >>> UTC = pytz.timezone("UTC")
-    >>> time_starts = datetime.datetime(2019, 6, 21, 0, 0, 0, 0, UTC)
-    >>> time_ends = datetime.datetime(2019, 7, 4, 0, 0, 0, 0, UTC)
+    >>> time_starts = datetime(2019, 6, 21, 0, 0, 0, 0, timezone.utc)
+    >>> time_ends = datetime(2019, 7, 4, 0, 0, 0, 0, timezone.utc)
     >>> sprint_attendance = SprintAttendance(sprint=sprint, attendee=person)
     >>> sprint_attendance.time_starts = time_starts
     >>> sprint_attendance.time_ends = time_ends
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 4ae2df1..1967768 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -11,8 +11,8 @@ __all__ = [
 ]
 
 import operator
+from datetime import timezone
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.lifecycle.objectdelta import ObjectDelta
 from storm.locals import (
@@ -207,7 +207,9 @@ class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
         name="owner", validator=validate_public_person, allow_none=False
     )
     owner = Reference(owner_id, "Person.id")
-    datecreated = DateTime(allow_none=False, default=DEFAULT, tzinfo=pytz.UTC)
+    datecreated = DateTime(
+        allow_none=False, default=DEFAULT, tzinfo=timezone.utc
+    )
     product_id = Int(name="product", allow_none=True, default=None)
     product = Reference(product_id, "Product.id")
     productseries_id = Int(name="productseries", allow_none=True, default=None)
@@ -229,7 +231,7 @@ class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
     )
     goal_proposer = Reference(goal_proposer_id, "Person.id")
     date_goal_proposed = DateTime(
-        allow_none=True, default=None, tzinfo=pytz.UTC
+        allow_none=True, default=None, tzinfo=timezone.utc
     )
     goal_decider_id = Int(
         name="goal_decider",
@@ -239,7 +241,7 @@ class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
     )
     goal_decider = Reference(goal_decider_id, "Person.id")
     date_goal_decided = DateTime(
-        allow_none=True, default=None, tzinfo=pytz.UTC
+        allow_none=True, default=None, tzinfo=timezone.utc
     )
     milestone_id = Int(name="milestone", allow_none=True, default=None)
     milestone = Reference(milestone_id, "Milestone.id")
@@ -261,7 +263,9 @@ class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
         default=None,
     )
     completer = Reference(completer_id, "Person.id")
-    date_completed = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_completed = DateTime(
+        allow_none=True, default=None, tzinfo=timezone.utc
+    )
     starter_id = Int(
         name="starter",
         allow_none=True,
@@ -269,7 +273,7 @@ class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
         default=None,
     )
     starter = Reference(starter_id, "Person.id")
-    date_started = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_started = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
 
     # useful joins
     _subscriptions = ReferenceSet(
diff --git a/lib/lp/blueprints/model/specificationbranch.py b/lib/lp/blueprints/model/specificationbranch.py
index 65f3f1a..aab2c7c 100644
--- a/lib/lp/blueprints/model/specificationbranch.py
+++ b/lib/lp/blueprints/model/specificationbranch.py
@@ -8,7 +8,8 @@ __all__ = [
     "SpecificationBranchSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Store
 from zope.interface import implementer
 
@@ -31,7 +32,10 @@ class SpecificationBranch(StormBase):
     id = Int(primary=True)
 
     datecreated = DateTime(
-        name="datecreated", tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW
+        name="datecreated",
+        tzinfo=timezone.utc,
+        allow_none=False,
+        default=UTC_NOW,
     )
     specification_id = Int(name="specification", allow_none=False)
     specification = Reference(specification_id, "Specification.id")
diff --git a/lib/lp/blueprints/model/sprint.py b/lib/lp/blueprints/model/sprint.py
index 8d31d8d..3a45f35 100644
--- a/lib/lp/blueprints/model/sprint.py
+++ b/lib/lp/blueprints/model/sprint.py
@@ -7,7 +7,8 @@ __all__ = [
     "HasSprintsMixin",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import (
     Bool,
     DateTime,
@@ -81,10 +82,12 @@ class Sprint(StormBase, HasDriversMixin, HasSpecificationsMixin):
     mugshot_id = Int(name="mugshot", default=None)
     mugshot = Reference(mugshot_id, "LibraryFileAlias.id")
     address = Unicode(allow_none=True, default=None)
-    datecreated = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    datecreated = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
     time_zone = Unicode(allow_none=False)
-    time_starts = DateTime(tzinfo=pytz.UTC, allow_none=False)
-    time_ends = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    time_starts = DateTime(tzinfo=timezone.utc, allow_none=False)
+    time_ends = DateTime(tzinfo=timezone.utc, allow_none=False)
     is_physical = Bool(allow_none=False, default=True)
 
     def __init__(
diff --git a/lib/lp/blueprints/model/sprintspecification.py b/lib/lp/blueprints/model/sprintspecification.py
index 9d90b13..6d826a2 100644
--- a/lib/lp/blueprints/model/sprintspecification.py
+++ b/lib/lp/blueprints/model/sprintspecification.py
@@ -3,7 +3,8 @@
 
 __all__ = ["SprintSpecification"]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Store, Unicode
 from zope.interface import implementer
 
@@ -37,7 +38,9 @@ class SprintSpecification(StormBase):
         name="registrant", validator=validate_public_person, allow_none=False
     )
     registrant = Reference(registrant_id, "Person.id")
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
     decider_id = Int(
         name="decider",
         validator=validate_public_person,
@@ -45,7 +48,7 @@ class SprintSpecification(StormBase):
         default=None,
     )
     decider = Reference(decider_id, "Person.id")
-    date_decided = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_decided = DateTime(tzinfo=timezone.utc, allow_none=True, default=None)
 
     def __init__(self, sprint, specification, registrant):
         super().__init__()
diff --git a/lib/lp/blueprints/model/tests/test_sprint.py b/lib/lp/blueprints/model/tests/test_sprint.py
index 4f186fa..7fdbcc9 100644
--- a/lib/lp/blueprints/model/tests/test_sprint.py
+++ b/lib/lp/blueprints/model/tests/test_sprint.py
@@ -3,9 +3,8 @@
 
 """Unit test for sprints."""
 
-import datetime
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -33,7 +32,7 @@ class TestSpecifications(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.date_decided = datetime.datetime.now(utc)
+        self.date_decided = datetime.now(timezone.utc)
 
     def makeSpec(
         self,
@@ -66,9 +65,9 @@ class TestSpecifications(TestCaseWithFactory):
         elif not proposed:
             link.acceptBy(sprint.owner)
         if not proposed:
-            date_decided = self.date_decided + datetime.timedelta(date_decided)
+            date_decided = self.date_decided + timedelta(date_decided)
             naked_link.date_decided = date_decided
-        date_created = self.date_decided + datetime.timedelta(date_created)
+        date_created = self.date_decided + timedelta(date_created)
         naked_link.date_created = date_created
         return blueprint
 
diff --git a/lib/lp/blueprints/stories/blueprints/xx-creation.rst b/lib/lp/blueprints/stories/blueprints/xx-creation.rst
index e141a50..e5e1e91 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-creation.rst
+++ b/lib/lp/blueprints/stories/blueprints/xx-creation.rst
@@ -181,17 +181,13 @@ From a sprint
 
 Starting from the Future Mega Meeting sprint page:
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
 
     >>> login("test@xxxxxxxxxxxxx")
     >>> _ = factory.makeSprint(
     ...     name="futurista",
     ...     title="Future Mega Meeting",
-    ...     time_starts=datetime.now(pytz.UTC) + timedelta(days=1),
+    ...     time_starts=datetime.now(timezone.utc) + timedelta(days=1),
     ... )
     >>> logout()
 
@@ -623,8 +619,8 @@ permission, the blueprint will be automatically added to the sprint agenda:
     >>> rome_sprint = factory.makeSprint(name="rome")
     >>> logout()
     >>> ignored = login_person(rome_sprint.owner)
-    >>> rome_sprint.time_ends = datetime.now(pytz.UTC) + timedelta(30)
-    >>> rome_sprint.time_starts = datetime.now(pytz.UTC) + timedelta(20)
+    >>> rome_sprint.time_ends = datetime.now(timezone.utc) + timedelta(30)
+    >>> rome_sprint.time_starts = datetime.now(timezone.utc) + timedelta(20)
     >>> sample_person = getUtility(IPersonSet).getByName("name12")
     >>> rome_sprint.driver = sample_person
     >>> logout()
diff --git a/lib/lp/blueprints/stories/sprints/sprint-settopics.rst b/lib/lp/blueprints/stories/sprints/sprint-settopics.rst
index 8885f68..1e015f8 100644
--- a/lib/lp/blueprints/stories/sprints/sprint-settopics.rst
+++ b/lib/lp/blueprints/stories/sprints/sprint-settopics.rst
@@ -1,16 +1,12 @@
 Any logged in user can propose specs to be discussed in a sprint.
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
 
     >>> login("test@xxxxxxxxxxxxx")
     >>> _ = factory.makeSprint(
     ...     name="uds-guacamole",
     ...     title="Ubuntu DevSummit Guacamole",
-    ...     time_starts=datetime.now(pytz.UTC) + timedelta(days=1),
+    ...     time_starts=datetime.now(timezone.utc) + timedelta(days=1),
     ... )
     >>> transaction.commit()
     >>> logout()
diff --git a/lib/lp/blueprints/stories/sprints/xx-sprints.rst b/lib/lp/blueprints/stories/sprints/xx-sprints.rst
index daaa223..647972d 100644
--- a/lib/lp/blueprints/stories/sprints/xx-sprints.rst
+++ b/lib/lp/blueprints/stories/sprints/xx-sprints.rst
@@ -6,16 +6,15 @@ due to be discussed at that meeting. As a result we can schedule and
 prioritize BOF's at the meeting, using an as-yet-undeveloped
 schedul-o-matic.
 
-    >>> import datetime as dt
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> login("test@xxxxxxxxxxxxx")
     >>> rome_sprint = factory.makeSprint(name="rome", title="Rome")
     >>> logout()
     >>> ignored = login_person(rome_sprint.owner)
-    >>> rome_sprint.time_ends = dt.datetime.now(UTC) + dt.timedelta(30)
-    >>> rome_sprint.time_starts = dt.datetime.now(UTC) + dt.timedelta(20)
+    >>> rome_sprint.time_ends = datetime.now(timezone.utc) + timedelta(30)
+    >>> rome_sprint.time_starts = datetime.now(timezone.utc) + timedelta(20)
     >>> sample_person = getUtility(IPersonSet).getByName("name12")
     >>> rome_sprint.driver = sample_person
     >>> logout()
diff --git a/lib/lp/blueprints/stories/standalone/sprint-links.rst b/lib/lp/blueprints/stories/standalone/sprint-links.rst
index a7dcf0e..4e3ba6c 100644
--- a/lib/lp/blueprints/stories/standalone/sprint-links.rst
+++ b/lib/lp/blueprints/stories/standalone/sprint-links.rst
@@ -6,16 +6,12 @@ agenda, and then it is approved. If, however, the person nominating the spec
 also has permission to approve it, then it should be approved automatically
 for the agenda.
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
 
     >>> login("test@xxxxxxxxxxxxx")
     >>> _ = factory.makeSprint(
     ...     name="uds-guacamole",
-    ...     time_starts=datetime.now(pytz.UTC) + timedelta(days=1),
+    ...     time_starts=datetime.now(timezone.utc) + timedelta(days=1),
     ... )
     >>> logout()
 
@@ -75,16 +71,14 @@ It's VERY IMPORTANT that this test pass because we cannot test this in
 doctests, since it depends on Browser View code to work (the database
 classes don't know about their own permissions and security).
 
-    >>> import datetime as dt
-    >>> from pytz import UTC
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> login("test@xxxxxxxxxxxxx")
     >>> rome_sprint = factory.makeSprint(name="rome")
     >>> logout()
     >>> ignored = login_person(rome_sprint.owner)
-    >>> rome_sprint.time_ends = dt.datetime.now(UTC) + dt.timedelta(30)
-    >>> rome_sprint.time_starts = dt.datetime.now(UTC) + dt.timedelta(20)
+    >>> rome_sprint.time_ends = datetime.now(timezone.utc) + timedelta(30)
+    >>> rome_sprint.time_starts = datetime.now(timezone.utc) + timedelta(20)
     >>> sample_person = getUtility(IPersonSet).getByName("name12")
     >>> rome_sprint.driver = sample_person
     >>> logout()
diff --git a/lib/lp/blueprints/tests/test_specification.py b/lib/lp/blueprints/tests/test_specification.py
index 62fbea3..2778005 100644
--- a/lib/lp/blueprints/tests/test_specification.py
+++ b/lib/lp/blueprints/tests/test_specification.py
@@ -3,9 +3,8 @@
 
 """Unit tests for Specification."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.store import Store
 from zope.component import getUtility, queryAdapter
 from zope.security.checker import CheckerPublic, getChecker
@@ -782,7 +781,7 @@ class TestSpecifications(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.date_created = datetime.now(pytz.utc)
+        self.date_created = datetime.now(timezone.utc)
 
     def makeSpec(
         self,
diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
index 0d7f4af..c706156 100644
--- a/lib/lp/bugs/browser/bugtarget.py
+++ b/lib/lp/bugs/browser/bugtarget.py
@@ -19,14 +19,13 @@ __all__ = [
 
 import http.client
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 from functools import partial
 from io import BytesIO
 from urllib.parse import quote, urlencode
 
 from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache
-from pytz import timezone
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.formlib.form import Fields
@@ -1471,7 +1470,7 @@ class BugsPatchesView(LaunchpadView):
 
     def patchAge(self, patch):
         """Return a timedelta object for the age of a patch attachment."""
-        now = datetime.now(timezone("UTC"))
+        now = datetime.now(timezone.utc)
         return now - patch.message.datecreated
 
 
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 161e405..f30dea5 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -28,7 +28,7 @@ __all__ = [
 import json
 import re
 from collections import defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from itertools import groupby
 from operator import attrgetter
 from typing import List
@@ -46,7 +46,6 @@ from lazr.restful.interfaces import (
     IWebServiceClientRequest,
 )
 from lazr.restful.utils import smartquote
-from pytz import utc
 from zope import formlib
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import adapter, getAdapter, getMultiAdapter, getUtility
@@ -872,7 +871,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
 
         expire_after = timedelta(days=config.malone.days_before_expiration)
         expiration_date = self.context.bug.date_last_updated + expire_after
-        remaining_time = expiration_date - datetime.now(utc)
+        remaining_time = expiration_date - datetime.now(timezone.utc)
         return remaining_time.days
 
     @property
diff --git a/lib/lp/bugs/browser/tests/bug-views.rst b/lib/lp/bugs/browser/tests/bug-views.rst
index 008659a..b1e522d 100644
--- a/lib/lp/bugs/browser/tests/bug-views.rst
+++ b/lib/lp/bugs/browser/tests/bug-views.rst
@@ -751,8 +751,7 @@ for a bug, ordered by date.
 
 First, some set-up.
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.bugs.adapters.bugchange import (
     ...     BugLocked,
     ...     BugLockReasonSet,
@@ -762,9 +761,7 @@ First, some set-up.
     ...     BugUnlocked,
     ... )
     >>> from lp.bugs.enums import BugLockStatus
-    >>> nowish = datetime(
-    ...     2009, 3, 26, 21, 37, 45, tzinfo=pytz.timezone("UTC")
-    ... )
+    >>> nowish = datetime(2009, 3, 26, 21, 37, 45, tzinfo=timezone.utc)
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> product = factory.makeProduct(name="testproduct")
diff --git a/lib/lp/bugs/browser/tests/bugwatch-views.rst b/lib/lp/bugs/browser/tests/bugwatch-views.rst
index 21da179..db74910 100644
--- a/lib/lp/bugs/browser/tests/bugwatch-views.rst
+++ b/lib/lp/bugs/browser/tests/bugwatch-views.rst
@@ -90,7 +90,7 @@ about the activity.
     ...     pprint(activity_dict)
     ...
     {'completion_message': 'completed successfully',
-     'date': datetime.datetime(...tzinfo=<UTC>),
+     'date': datetime.datetime(...tzinfo=datetime.timezone.utc),
      'icon': '/@@/yes',
      'oops_id': None,
      'result_text': 'Synchronisation succeeded'}
@@ -111,12 +111,12 @@ different dates.
     ...     pprint(activity_dict)
     ...
     {'completion_message': "failed with error 'Bug Not Found'",
-     'date': datetime.datetime(...tzinfo=<UTC>),
+     'date': datetime.datetime(...tzinfo=datetime.timezone.utc),
      'icon': '/@@/no',
      'oops_id': None,
      'result_text': 'Bug Not Found'}
     {'completion_message': 'completed successfully',
-     'date': datetime.datetime(...tzinfo=<UTC>),
+     'date': datetime.datetime(...tzinfo=datetime.timezone.utc),
      'icon': '/@@/yes',
      'oops_id': None,
      'result_text': 'Synchronisation succeeded'}
diff --git a/lib/lp/bugs/browser/tests/test_bug_views.py b/lib/lp/bugs/browser/tests/test_bug_views.py
index d198ad0..213887a 100644
--- a/lib/lp/bugs/browser/tests/test_bug_views.py
+++ b/lib/lp/bugs/browser/tests/test_bug_views.py
@@ -5,9 +5,8 @@
 
 import json
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from soupmatchers import HTMLContains, Tag, Within
 from storm.store import Store
 from testtools.matchers import Contains, Equals, MatchesAll, Not
@@ -964,7 +963,7 @@ class TestBugActivityView(TestCaseWithFactory):
         # Bug:+activity doesn't make O(n) queries based on the amount of
         # activity.
         bug = self.factory.makeBug()
-        ten_minutes_ago = datetime.now(pytz.UTC) - timedelta(minutes=10)
+        ten_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=10)
         with person_logged_in(bug.owner):
             attachment = self.factory.makeBugAttachment(bug=bug)
             for i in range(10):
diff --git a/lib/lp/bugs/browser/tests/test_bugcomment.py b/lib/lp/bugs/browser/tests/test_bugcomment.py
index db14239..24d664c 100644
--- a/lib/lp/bugs/browser/tests/test_bugcomment.py
+++ b/lib/lp/bugs/browser/tests/test_bugcomment.py
@@ -3,10 +3,9 @@
 
 """Tests for the bugcomment module."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from itertools import count
 
-from pytz import utc
 from soupmatchers import HTMLContains, Tag
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -80,7 +79,7 @@ class TestGroupCommentsWithActivities(TestCase):
 
     def setUp(self):
         super().setUp()
-        self.now = datetime.now(utc)
+        self.now = datetime.now(timezone.utc)
         self.time_index = (
             (self.now + timedelta(minutes=counter), counter)
             for counter in count(1)
diff --git a/lib/lp/bugs/browser/tests/test_bugtask.py b/lib/lp/bugs/browser/tests/test_bugtask.py
index 724af22..3bfbb38 100644
--- a/lib/lp/bugs/browser/tests/test_bugtask.py
+++ b/lib/lp/bugs/browser/tests/test_bugtask.py
@@ -3,13 +3,12 @@
 
 import json
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.parse import urlencode
 
 import soupmatchers
 import transaction
 from lazr.restful.interfaces import IJSONRequestCache
-from pytz import UTC
 from testscenarios import WithScenarios, load_tests_apply_scenarios
 from testtools.matchers import LessThan, Not
 from zope.component import getMultiAdapter, getUtility
@@ -175,7 +174,7 @@ class TestBugTaskView(TestCaseWithFactory):
         def add_activity(what, old=None, new=None, message=None):
             getUtility(IBugActivitySet).new(
                 bug,
-                datetime.now(UTC),
+                datetime.now(timezone.utc),
                 bug.owner,
                 whatchanged=what,
                 oldvalue=old,
@@ -205,7 +204,7 @@ class TestBugTaskView(TestCaseWithFactory):
 
         def add_activity(what, who):
             getUtility(IBugActivitySet).new(
-                task.bug, datetime.now(UTC), who, whatchanged=what
+                task.bug, datetime.now(timezone.utc), who, whatchanged=what
             )
 
         recorder1, recorder2 = record_two_runs(
@@ -709,7 +708,9 @@ class TestBugTasksTableView(TestCaseWithFactory):
             version="2.0",
             component=component,
             sourcepackagename=spn,
-            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
+            date_uploaded=datetime(
+                2008, 7, 18, 10, 20, 30, tzinfo=timezone.utc
+            ),
             maintainer=maintainer,
             spr_creator=creator,
         )
@@ -738,7 +739,9 @@ class TestBugTasksTableView(TestCaseWithFactory):
             version="2.0",
             component=component,
             sourcepackagename=spn,
-            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
+            date_uploaded=datetime(
+                2008, 7, 18, 10, 20, 30, tzinfo=timezone.utc
+            ),
             maintainer=maintainer,
             spr_creator=creator,
         )
@@ -767,7 +770,9 @@ class TestBugTasksTableView(TestCaseWithFactory):
             version="2.0",
             component=component,
             sourcepackagename=spn,
-            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
+            date_uploaded=datetime(
+                2008, 7, 18, 10, 20, 30, tzinfo=timezone.utc
+            ),
             maintainer=maintainer,
             spr_creator=creator,
         )
@@ -3085,7 +3090,7 @@ class TestBugTaskListingItem(TestCaseWithFactory):
         """Model contains bug age."""
         owner, item = make_bug_task_listing_item(self.factory)
         bug = removeSecurityProxy(item.bug)
-        bug.datecreated = datetime.now(UTC) - timedelta(3, 0, 0)
+        bug.datecreated = datetime.now(timezone.utc) - timedelta(3, 0, 0)
         with person_logged_in(owner):
             self.assertEqual("3 days old", item.model["age"])
 
@@ -3114,8 +3119,8 @@ class TestBugTaskListingItem(TestCaseWithFactory):
         owner, item = make_bug_task_listing_item(self.factory)
         with person_logged_in(owner):
             bug = removeSecurityProxy(item.bug)
-            bug.date_last_updated = datetime(2001, 1, 1, tzinfo=UTC)
-            bug.date_last_message = datetime(2000, 1, 1, tzinfo=UTC)
+            bug.date_last_updated = datetime(2001, 1, 1, tzinfo=timezone.utc)
+            bug.date_last_message = datetime(2000, 1, 1, tzinfo=timezone.utc)
             self.assertEqual("on 2001-01-01", item.model["last_updated"])
 
     def test_model_last_updated_date_last_message(self):
@@ -3123,8 +3128,8 @@ class TestBugTaskListingItem(TestCaseWithFactory):
         owner, item = make_bug_task_listing_item(self.factory)
         with person_logged_in(owner):
             bug = removeSecurityProxy(item.bug)
-            bug.date_last_updated = datetime(2000, 1, 1, tzinfo=UTC)
-            bug.date_last_message = datetime(2001, 1, 1, tzinfo=UTC)
+            bug.date_last_updated = datetime(2000, 1, 1, tzinfo=timezone.utc)
+            bug.date_last_message = datetime(2001, 1, 1, tzinfo=timezone.utc)
             self.assertEqual("on 2001-01-01", item.model["last_updated"])
 
 
diff --git a/lib/lp/bugs/doc/bug-change.rst b/lib/lp/bugs/doc/bug-change.rst
index de42def..19309cf 100644
--- a/lib/lp/bugs/doc/bug-change.rst
+++ b/lib/lp/bugs/doc/bug-change.rst
@@ -3,9 +3,8 @@ Tracking changes to a bug
 
 The base class for BugChanges doesn't actually implement anything.
 
-    >>> import pytz
     >>> from lp.testing import verifyObject
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.bugs.adapters.bugchange import BugChangeBase
     >>> from lp.bugs.interfaces.bugchange import IBugChange
 
@@ -16,7 +15,7 @@ The base class for BugChanges doesn't actually implement anything.
     ...     name="ford-prefect", displayname="Ford Prefect"
     ... )
 
-    >>> nowish = datetime(2009, 3, 13, 10, 9, tzinfo=pytz.timezone("UTC"))
+    >>> nowish = datetime(2009, 3, 13, 10, 9, tzinfo=timezone.utc)
     >>> base_instance = BugChangeBase(when=nowish, person=example_person)
     >>> verifyObject(IBugChange, base_instance)
     True
diff --git a/lib/lp/bugs/doc/bug-heat.rst b/lib/lp/bugs/doc/bug-heat.rst
index 759b25e..a95293a 100644
--- a/lib/lp/bugs/doc/bug-heat.rst
+++ b/lib/lp/bugs/doc/bug-heat.rst
@@ -143,8 +143,7 @@ We'll set the bug's heat to 0 first to demonstrate this.
 
     >>> removeSecurityProxy(bug).heat = 0
 
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import timezone
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.services.utils import utc_now
 
     >>> from lp.bugs.adapters.bugchange import BugDescriptionChange
@@ -174,7 +173,7 @@ out of date.
     >>> from lp.bugs.model.bug import Bug
     >>> from lp.services.database.interfaces import IStore
     >>> IStore(Bug).find(Bug).set(
-    ...     heat_last_updated=datetime.now(timezone("UTC"))
+    ...     heat_last_updated=datetime.now(timezone.utc)
     ... )
 
 If we call getBugsWithOutdatedHeat() now, the set that is returned will
@@ -182,7 +181,7 @@ be empty because all the bugs have been recently updated.
 getBugsWithOutdatedHeat() takes a single parameter, cutoff, which is the
 oldest a bug's heat can be before it gets included in the returned set.
 
-    >>> yesterday = datetime.now(timezone("UTC")) - timedelta(days=1)
+    >>> yesterday = datetime.now(timezone.utc) - timedelta(days=1)
     >>> getUtility(IBugSet).getBugsWithOutdatedHeat(yesterday).count()
     0
 
@@ -193,9 +192,9 @@ getBugsWithOutdatedHeat().
     >>> old_heat_bug = factory.makeBug()
     >>> naked_bug = removeSecurityProxy(old_heat_bug)
     >>> naked_bug.heat = 0
-    >>> naked_bug.heat_last_updated = datetime.now(
-    ...     timezone("UTC")
-    ... ) - timedelta(days=2)
+    >>> naked_bug.heat_last_updated = datetime.now(timezone.utc) - timedelta(
+    ...     days=2
+    ... )
 
     >>> outdated_bugs = getUtility(IBugSet).getBugsWithOutdatedHeat(yesterday)
     >>> outdated_bugs.count()
diff --git a/lib/lp/bugs/doc/bugactivity.rst b/lib/lp/bugs/doc/bugactivity.rst
index bdbe87a..e07d45f 100644
--- a/lib/lp/bugs/doc/bugactivity.rst
+++ b/lib/lp/bugs/doc/bugactivity.rst
@@ -281,15 +281,12 @@ BugActivityItem
 BugActivityItem implements the stuff that BugActivity doesn't need to
 know about.
 
-    >>> import pytz
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.bugs.browser.bugtask import BugActivityItem
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> from lp.bugs.interfaces.bugactivity import IBugActivitySet
 
-    >>> nowish = datetime(
-    ...     2009, 3, 26, 16, 40, 31, tzinfo=pytz.timezone("UTC")
-    ... )
+    >>> nowish = datetime(2009, 3, 26, 16, 40, 31, tzinfo=timezone.utc)
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> activity = getUtility(IBugActivitySet).new(
     ...     bug=bug_one,
diff --git a/lib/lp/bugs/doc/bugnotification-sending.rst b/lib/lp/bugs/doc/bugnotification-sending.rst
index 59e1d27..abf8f70 100644
--- a/lib/lp/bugs/doc/bugnotification-sending.rst
+++ b/lib/lp/bugs/doc/bugnotification-sending.rst
@@ -8,9 +8,8 @@ an email notification, and sent to the appropriate people.
 Before we start, let's ensure that there are no pending notifications to
 be sent:
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> ten_minutes_ago = now - timedelta(minutes=10)
     >>> from lp.bugs.interfaces.bugnotification import IBugNotificationSet
     >>> len(getUtility(IBugNotificationSet).getNotificationsToSend())
@@ -133,7 +132,7 @@ still None:
 Setting date_emailed to some date causes it not to be pending anymore:
 
     >>> from lp.services.database.sqlbase import flush_database_updates
-    >>> notifications[0].date_emailed = datetime.now(pytz.timezone("UTC"))
+    >>> notifications[0].date_emailed = datetime.now(timezone.utc)
     >>> flush_database_updates()
     >>> pending_notifications = getUtility(
     ...     IBugNotificationSet
@@ -144,7 +143,7 @@ Setting date_emailed to some date causes it not to be pending anymore:
 Let's define a helper function to do that for all pending notifications:
 
     >>> def flush_notifications():
-    ...     utc_now = datetime.now(pytz.timezone("UTC"))
+    ...     utc_now = datetime.now(timezone.utc)
     ...     pending_notifications = getUtility(
     ...         IBugNotificationSet
     ...     ).getNotificationsToSend()
@@ -338,7 +337,7 @@ signature, and the signature marker has a trailing space.
 We send the notification only if the user hasn't done any other changes
 for the last 5 minutes:
 
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> for minutes_ago in reversed(range(10)):
     ...     bug_one.addChange(
     ...         BugInformationTypeChange(
@@ -1103,7 +1102,7 @@ them:
     1
 
     >>> for notification in notifications:
-    ...     notification.date_emailed = datetime.now(pytz.timezone("UTC"))
+    ...     notification.date_emailed = datetime.now(timezone.utc)
     ...
 
 
diff --git a/lib/lp/bugs/doc/bugnotification-threading.rst b/lib/lp/bugs/doc/bugnotification-threading.rst
index 648c9a0..e69c80c 100644
--- a/lib/lp/bugs/doc/bugnotification-threading.rst
+++ b/lib/lp/bugs/doc/bugnotification-threading.rst
@@ -14,16 +14,13 @@ Let's add add change notification and see how it works:
 
     >>> login("test@xxxxxxxxxxxxx")
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.services.messages.interfaces.message import IMessageSet
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> from lp.bugs.adapters.bugchange import BugInformationTypeChange
     >>> from lp.app.enums import InformationType
 
-    >>> ten_minutes_ago = datetime.now(pytz.timezone("UTC")) - timedelta(
-    ...     minutes=10
-    ... )
+    >>> ten_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=10)
     >>> sample_person = getUtility(ILaunchBag).user
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> bug_one.addChange(
@@ -113,7 +110,7 @@ Refresh the dates, and create a new reply to ensure that the references
 are chained together properly:
 
     >>> for notification in notifications:
-    ...     notification.date_emailed = datetime.now(pytz.timezone("UTC"))
+    ...     notification.date_emailed = datetime.now(timezone.utc)
     ...
     >>> flush_database_updates()
 
@@ -159,7 +156,7 @@ References header.
     >>> from lp.services.database.interfaces import IStore
 
     >>> for notification in notifications:
-    ...     notification.date_emailed = datetime.now(pytz.timezone("UTC"))
+    ...     notification.date_emailed = datetime.now(timezone.utc)
     ...
     >>> flush_database_updates()
 
diff --git a/lib/lp/bugs/doc/bugtask-expiration.rst b/lib/lp/bugs/doc/bugtask-expiration.rst
index 1ba6fc2..3c8fa87 100644
--- a/lib/lp/bugs/doc/bugtask-expiration.rst
+++ b/lib/lp/bugs/doc/bugtask-expiration.rst
@@ -59,11 +59,11 @@ to be last modified in the past.
 
     >>> from zope.security.proxy import removeSecurityProxy
     >>> def reset_bug_modified_date(bug, days_ago):
-    ...     from datetime import datetime, timedelta
-    ...     import pytz
+    ...     from datetime import datetime, timedelta, timezone
     ...
-    ...     UTC = pytz.timezone("UTC")
-    ...     date_modified = datetime.now(UTC) - timedelta(days=days_ago)
+    ...     date_modified = datetime.now(timezone.utc) - timedelta(
+    ...         days=days_ago
+    ...     )
     ...     removeSecurityProxy(bug).date_last_updated = date_modified
     ...
 
diff --git a/lib/lp/bugs/doc/bugtracker.rst b/lib/lp/bugs/doc/bugtracker.rst
index 89363fb..cd12b35 100644
--- a/lib/lp/bugs/doc/bugtracker.rst
+++ b/lib/lp/bugs/doc/bugtracker.rst
@@ -6,13 +6,12 @@ document discusses the API of external bug trackers. To learn more about
 bug watches, the object that represents the link between a Malone bug
 and an external bug, see bugwatch.rst.
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.services.database.sqlbase import flush_database_updates
     >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet
     >>> bugtracker_set = getUtility(IBugTrackerSet)
     >>> mozilla_bugzilla = bugtracker_set.getByName("mozilla.org")
-    >>> now = datetime.now(pytz.UTC)
+    >>> now = datetime.now(timezone.utc)
 
     >>> def print_watches(bugtracker):
     ...     watches = sorted(
diff --git a/lib/lp/bugs/doc/bugwatch.rst b/lib/lp/bugs/doc/bugwatch.rst
index 465548c..29e0f2b 100644
--- a/lib/lp/bugs/doc/bugwatch.rst
+++ b/lib/lp/bugs/doc/bugwatch.rst
@@ -620,9 +620,8 @@ If the watch is rescheduled, can_be_rescheduled will be False, since the
 next_check time for the watch will be in the past (or in this case is
 now) and therefore it will be checked with the next checkwatches run.
 
-    >>> from datetime import datetime
-    >>> from pytz import utc
-    >>> schedulable_watch.next_check = datetime.now(utc)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> schedulable_watch.next_check = datetime.now(timezone.utc)
     >>> schedulable_watch.can_be_rescheduled
     False
 
@@ -641,11 +640,10 @@ needs attention in order for it to be able to work again.
 If the watch has run and failed only once, can_be_rescheduled will be
 true.
 
-    >>> from datetime import timedelta
     >>> run_once_failed_once_watch = factory.makeBugWatch()
-    >>> run_once_failed_once_watch.next_check = datetime.now(utc) + timedelta(
-    ...     days=7
-    ... )
+    >>> run_once_failed_once_watch.next_check = datetime.now(
+    ...     timezone.utc
+    ... ) + timedelta(days=7)
     >>> run_once_failed_once_watch.addActivity(
     ...     result=BugWatchActivityStatus.BUG_NOT_FOUND
     ... )
@@ -680,7 +678,7 @@ be rescheduled.
 Calling setNextCheck() on this watch will cause an Exception,
 BugWatchCannotBeRescheduled, to be raised.
 
-    >>> schedulable_watch.setNextCheck(datetime.now(utc))
+    >>> schedulable_watch.setNextCheck(datetime.now(timezone.utc))
     Traceback (most recent call last):
       ...
     lp.bugs.interfaces.bugwatch.BugWatchCannotBeRescheduled
@@ -694,7 +692,7 @@ property become True, setNextCheck() will succeed.
     >>> schedulable_watch.can_be_rescheduled
     True
 
-    >>> next_check = datetime.now(utc)
+    >>> next_check = datetime.now(timezone.utc)
     >>> schedulable_watch.setNextCheck(next_check)
     >>> schedulable_watch.next_check == next_check
     True
diff --git a/lib/lp/bugs/doc/checkwatches-batching.rst b/lib/lp/bugs/doc/checkwatches-batching.rst
index 21c7151..2b949c8 100644
--- a/lib/lp/bugs/doc/checkwatches-batching.rst
+++ b/lib/lp/bugs/doc/checkwatches-batching.rst
@@ -77,8 +77,7 @@ For bug watches that have been checked before, the remote system is
 asked which of a list of bugs have been modified since a given date.
 
     >>> from zope.security.proxy import removeSecurityProxy
-    >>> from datetime import datetime
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timezone
 
     >>> class QueryableRemoteSystem:
     ...     sync_comments = False
@@ -93,7 +92,7 @@ asked which of a list of bugs have been modified since a given date.
     ...
 
     >>> remote = QueryableRemoteSystem()
-    >>> now = datetime(2010, 1, 13, 16, 52, tzinfo=UTC)
+    >>> now = datetime(2010, 1, 13, 16, 52, tzinfo=timezone.utc)
 
 When there are no bug watches to check, the result is empty, and the
 remote system is not queried.
diff --git a/lib/lp/bugs/doc/checkwatches-cli-switches.rst b/lib/lp/bugs/doc/checkwatches-cli-switches.rst
index 9e35687..780cc27 100644
--- a/lib/lp/bugs/doc/checkwatches-cli-switches.rst
+++ b/lib/lp/bugs/doc/checkwatches-cli-switches.rst
@@ -90,8 +90,7 @@ a user needs to pass the '--reset' option to the checkwatches cron script.
 First, lets add some bug watches to the Savannah bug tracker to
 demonstrate this.
 
-    >>> import pytz
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet
     >>> from lp.testing.factory import LaunchpadObjectFactory
 
@@ -101,7 +100,7 @@ demonstrate this.
     ...     savannah = getUtility(IBugTrackerSet).getByName("savannah")
     ...     for i in range(5):
     ...         bug_watch = factory.makeBugWatch(bugtracker=savannah)
-    ...         bug_watch.lastchecked = datetime.now(pytz.timezone("UTC"))
+    ...         bug_watch.lastchecked = datetime.now(timezone.utc)
     ...
 
     >>> run_cronscript_with_args(["-vvt", "savannah", "--reset"])
diff --git a/lib/lp/bugs/doc/checkwatches.rst b/lib/lp/bugs/doc/checkwatches.rst
index 6969dc5..9b5eb82 100644
--- a/lib/lp/bugs/doc/checkwatches.rst
+++ b/lib/lp/bugs/doc/checkwatches.rst
@@ -19,12 +19,11 @@ checkwatches cronscript machinery directly.
 
 First, we create some bug watches to test with:
 
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> from lp.bugs.interfaces.bugtracker import BugTrackerType
     >>> from lp.bugs.model.bugtracker import BugTracker
-    >>> from pytz import utc
     >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> sample_person = getUtility(IPersonSet).getByEmail(
@@ -50,7 +49,7 @@ First, we create some bug watches to test with:
     ...     "1",
     ...     getUtility(ILaunchpadCelebrities).janitor,
     ... )
-    >>> example_bugwatch.next_check = datetime.now(utc)
+    >>> example_bugwatch.next_check = datetime.now(timezone.utc)
 
     >>> login("no-priv@xxxxxxxxxxxxx")
 
@@ -114,7 +113,7 @@ of the externalbugtracker module to ensure that it raises this error.
     ...
 
     >>> login(ANONYMOUS)
-    >>> example_bugwatch.next_check = datetime.now(utc)
+    >>> example_bugwatch.next_check = datetime.now(timezone.utc)
     >>> try:
     ...     externalbugtracker.get_external_bugtracker = (
     ...         broken_get_external_bugtracker
@@ -153,7 +152,7 @@ transaction if something goes wrong.
     ...         str(bug_id),
     ...         getUtility(ILaunchpadCelebrities).janitor,
     ...     )
-    ...     example_bugwatch.next_check = datetime.now(utc)
+    ...     example_bugwatch.next_check = datetime.now(timezone.utc)
     ...
 
 Since we know how many bugwatches example_bug has we will be able to see
@@ -292,7 +291,7 @@ made.
     ...             watches_to_update = bug_tracker.watches_needing_update[
     ...                 :batch_size
     ...             ]
-    ...             now = datetime.now(utc)
+    ...             now = datetime.now(timezone.utc)
     ...             for watch in watches_to_update:
     ...                 watch.lastchecked = now
     ...                 watch.next_check = None
@@ -337,7 +336,7 @@ We'll create a non-functioning ExternalBugtracker to demonstrate this.
     ...     ExternalBugTracker,
     ... )
 
-    >>> nowish = datetime.now(utc)
+    >>> nowish = datetime.now(timezone.utc)
     >>> @implementer(
     ...     ISupportsBackLinking,
     ...     ISupportsCommentImport,
diff --git a/lib/lp/bugs/doc/externalbugtracker-bugzilla-api.rst b/lib/lp/bugs/doc/externalbugtracker-bugzilla-api.rst
index d647a20..308a555 100644
--- a/lib/lp/bugs/doc/externalbugtracker-bugzilla-api.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-bugzilla-api.rst
@@ -108,7 +108,7 @@ to get the current time on the remote server.
     >>> test_transport.local_datetime = remote_time
     >>> bugzilla.getCurrentDBTime()
     CALLED Bugzilla.time()
-    datetime.datetime(2009, 8, 19, 17, 2, 2, tzinfo=<UTC>)
+    datetime.datetime(2009, 8, 19, 17, 2, 2, tzinfo=datetime.timezone.utc)
 
 If the remote system is in a different timezone, getCurrentDBTime() will
 convert its time to UTC before returning it.
@@ -117,7 +117,7 @@ convert its time to UTC before returning it.
     >>> test_transport.timezone = "CET"
     >>> bugzilla.getCurrentDBTime()
     CALLED Bugzilla.time()
-    datetime.datetime(2009, 8, 19, 16, 2, 2, tzinfo=<UTC>)
+    datetime.datetime(2009, 8, 19, 16, 2, 2, tzinfo=datetime.timezone.utc)
 
 This works whether the UTC offset is positive or negative.
 
@@ -125,7 +125,7 @@ This works whether the UTC offset is positive or negative.
     >>> test_transport.timezone = "US/Eastern"
     >>> bugzilla.getCurrentDBTime()
     CALLED Bugzilla.time()
-    datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=<UTC>)
+    datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=datetime.timezone.utc)
 
 Bugzilla >= 3.6 guarantees that db_time and web_time are in UTC, and
 Bugzilla >= 5.1.1 drops the web_time_utc flag.  We can cope with that.
@@ -133,7 +133,7 @@ Bugzilla >= 5.1.1 drops the web_time_utc flag.  We can cope with that.
     >>> test_transport.include_utc_time_fields = False
     >>> bugzilla.getCurrentDBTime()
     CALLED Bugzilla.time()
-    datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=<UTC>)
+    datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=datetime.timezone.utc)
 
 
 Initializing the bug database
diff --git a/lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.rst b/lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.rst
index 5e88dad..8c164dc 100644
--- a/lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.rst
@@ -191,7 +191,7 @@ and work with that.
     >>> test_transport.timezone = "CET"
     >>> test_transport.local_datetime = remote_time
     >>> bugzilla.getCurrentDBTime()
-    datetime.datetime(2008, 5, 16, 15, 53, 20, tzinfo=<UTC>)
+    datetime.datetime(2008, 5, 16, 15, 53, 20, tzinfo=datetime.timezone.utc)
 
 
 Initializing the remote bug database
diff --git a/lib/lp/bugs/doc/externalbugtracker-bugzilla.rst b/lib/lp/bugs/doc/externalbugtracker-bugzilla.rst
index f3f09ab..400ea49 100644
--- a/lib/lp/bugs/doc/externalbugtracker-bugzilla.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-bugzilla.rst
@@ -700,10 +700,9 @@ now no bug watches are in need of updating:
 If the status isn't different, the lastchanged attribute doesn't get
 updated:
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> bug_watch = gnome_bugzilla.watches[0]
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> bug_watch.lastchanged = now - timedelta(weeks=2)
     >>> old_last_changed = bug_watch.lastchanged
     >>> with external_bugzilla.responses(get=False):
diff --git a/lib/lp/bugs/doc/externalbugtracker-comment-imports.rst b/lib/lp/bugs/doc/externalbugtracker-comment-imports.rst
index 240edbc..eb59826 100644
--- a/lib/lp/bugs/doc/externalbugtracker-comment-imports.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-comment-imports.rst
@@ -507,9 +507,8 @@ comments.
     ...     return new_notifications
     ...
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
-    >>> now = datetime(2008, 9, 12, 15, 30, 45, tzinfo=pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime(2008, 9, 12, 15, 30, 45, tzinfo=timezone.utc)
     >>> with lp_dbuser():
     ...     test_bug = factory.makeBug(date_created=now)
     ...     bug_watch = factory.makeBugWatch("42", bug=test_bug)
diff --git a/lib/lp/bugs/doc/externalbugtracker-debbugs.rst b/lib/lp/bugs/doc/externalbugtracker-debbugs.rst
index 80bd099..d35b5e3 100644
--- a/lib/lp/bugs/doc/externalbugtracker-debbugs.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-debbugs.rst
@@ -106,10 +106,9 @@ That's because the check for whether a bug watch needs to be updated
 looks at its next_check field, which is None by default. Updating the
 bug watches should solve that problem.
 
-    >>> from datetime import datetime
-    >>> from pytz import utc
+    >>> from datetime import datetime, timezone
     >>> for watch in debbugs.watches:
-    ...     watch.next_check = datetime.now(utc)
+    ...     watch.next_check = datetime.now(timezone.utc)
     ...
 
     >>> bug_watches = list(debbugs.watches_needing_update)
@@ -595,7 +594,7 @@ correct date.
 
     >>> test_message["date"] = "Mon, 14 Jul 2008 21:10:10 +0100"
     >>> external_debbugs._getDateForComment(test_message)
-    datetime.datetime(2008, 7, 14, 20, 10, 10, tzinfo=<UTC>)
+    datetime.datetime(2008, 7, 14, 20, 10, 10, tzinfo=datetime.timezone.utc)
 
 If we add a Received header that isn't related to the domain of the
 current instance, the Date header will still have precedence.
@@ -604,7 +603,7 @@ current instance, the Date header will still have precedence.
     ...     "received"
     ... ] = "by thiswontwork.com; Tue, 15 Jul 2008 09:12:11 +0100"
     >>> external_debbugs._getDateForComment(test_message)
-    datetime.datetime(2008, 7, 14, 20, 10, 10, tzinfo=<UTC>)
+    datetime.datetime(2008, 7, 14, 20, 10, 10, tzinfo=datetime.timezone.utc)
 
 If there's a Received header that references the correct domain, the
 date in that header will take precedence.
@@ -613,7 +612,7 @@ date in that header will take precedence.
     ...     "received"
     ... ] = "by example.com; Tue, 15 Jul 2008 10:20:11 +0100"
     >>> external_debbugs._getDateForComment(test_message)
-    datetime.datetime(2008, 7, 15, 9, 20, 11, tzinfo=<UTC>)
+    datetime.datetime(2008, 7, 15, 9, 20, 11, tzinfo=datetime.timezone.utc)
 
 
 Pushing comments to DebBugs
diff --git a/lib/lp/bugs/doc/externalbugtracker-mantis-csv.rst b/lib/lp/bugs/doc/externalbugtracker-mantis-csv.rst
index 86611cc..a343d0e 100644
--- a/lib/lp/bugs/doc/externalbugtracker-mantis-csv.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-mantis-csv.rst
@@ -196,10 +196,9 @@ now no bug watches are in need of updating:
 If the status isn't different, the lastchanged attribute doesn't get
 updated:
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> bug_watch = example_bug_tracker.watches[0]
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> bug_watch.lastchanged = now - timedelta(weeks=2)
     >>> old_last_changed = bug_watch.lastchanged
     >>> bug_watch_updater.updateBugWatches(mantis, [bug_watch])
diff --git a/lib/lp/bugs/doc/externalbugtracker-mantis.rst b/lib/lp/bugs/doc/externalbugtracker-mantis.rst
index 2c2ac50..52a85eb 100644
--- a/lib/lp/bugs/doc/externalbugtracker-mantis.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-mantis.rst
@@ -207,10 +207,9 @@ now no bug watches are in need of updating:
 If the status isn't different, the lastchanged attribute doesn't get
 updated:
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> bug_watch = sorted(example_bug_tracker.watches, key=getid)[0]
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> bug_watch.lastchanged = now - timedelta(weeks=2)
     >>> bug_watch.lastchecked = bug_watch.lastchanged
     >>> old_last_changed = bug_watch.lastchanged
diff --git a/lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.rst b/lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.rst
index ad69fb4..553544d 100644
--- a/lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.rst
@@ -185,7 +185,7 @@ around with the TZ environment variable.
     ...
     Using XML-RPC to generate token.
     Successfully validated the token.
-    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
+    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=datetime.timezone.utc)
 
 An authorization request was automatically sent, since the method needed
 authentication. Because the cookie is now set, other calls won't cause
@@ -194,7 +194,7 @@ an authorization request.
     >>> test_transport.auth_cookie
     Cookie(version=0, name=...'trac_auth'...)
     >>> trac.getCurrentDBTime()
-    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
+    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=datetime.timezone.utc)
 
 If the cookie gets expired, an authorization request is automatically
 sent again.
@@ -205,7 +205,7 @@ sent again.
     ...
     Using XML-RPC to generate token.
     Successfully validated the token.
-    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=<UTC>)
+    datetime.datetime(2008, 4, 9, 2, 2, 1, tzinfo=datetime.timezone.utc)
 
 
 Getting modified bugs
diff --git a/lib/lp/bugs/doc/externalbugtracker-trac.rst b/lib/lp/bugs/doc/externalbugtracker-trac.rst
index 80598c2..a230662 100644
--- a/lib/lp/bugs/doc/externalbugtracker-trac.rst
+++ b/lib/lp/bugs/doc/externalbugtracker-trac.rst
@@ -422,15 +422,14 @@ updated. If we set a bug watch's lastchanged timestamp manually and call
 update, lastchanged shouldn't be affected because the remote status of the bug
 watch hasn't altered:
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from operator import attrgetter
     >>> sorted_bug_watches = sorted(
     ...     (bug_watch for bug_watch in example_bug_tracker.watches),
     ...     key=attrgetter("remotebug"),
     ... )
     >>> bug_watch = sorted_bug_watches[-1]
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> bug_watch.lastchanged = now - timedelta(weeks=2)
     >>> old_last_changed = bug_watch.lastchanged
     >>> print(bug_watch.remotebug)
diff --git a/lib/lp/bugs/doc/externalbugtracker.rst b/lib/lp/bugs/doc/externalbugtracker.rst
index fe920fb..84a1835 100644
--- a/lib/lp/bugs/doc/externalbugtracker.rst
+++ b/lib/lp/bugs/doc/externalbugtracker.rst
@@ -362,8 +362,8 @@ If the difference between what we and the remote system think the time
 is, an error is raised.
 
     >>> import pytz
-    >>> from datetime import datetime, timedelta
-    >>> utc_now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> utc_now = datetime.now(timezone.utc)
     >>> class PositiveTimeSkewExternalBugTracker(TestExternalBugTracker):
     ...     def getCurrentDBTime(self):
     ...         return utc_now + timedelta(minutes=20)
@@ -469,7 +469,7 @@ know that their time is similar to ours.
 
     >>> class CheckModifiedExternalBugTracker(InitializingExternalBugTracker):
     ...     def getCurrentDBTime(self):
-    ...         return datetime.now(pytz.timezone("UTC"))
+    ...         return datetime.now(timezone.utc)
     ...
     ...     def getModifiedRemoteBugs(self, remote_bug_ids, last_checked):
     ...         print("last_checked: %s" % last_checked)
@@ -570,9 +570,7 @@ If the bug watches have the lastchecked attribute set, they will be
 passed to getModifiedRemoteBugs(). Only the bugs that have been modified
 will then be passed on to initializeRemoteBugDB().
 
-    >>> some_time_ago = datetime(
-    ...     2007, 3, 17, 16, 0, tzinfo=pytz.timezone("UTC")
-    ... )
+    >>> some_time_ago = datetime(2007, 3, 17, 16, 0, tzinfo=timezone.utc)
     >>> for bug_watch in bug_watches:
     ...     bug_watch.lastchecked = some_time_ago
     ...
diff --git a/lib/lp/bugs/externalbugtracker/bugzilla.py b/lib/lp/bugs/externalbugtracker/bugzilla.py
index 85feefa..da5e18f 100644
--- a/lib/lp/bugs/externalbugtracker/bugzilla.py
+++ b/lib/lp/bugs/externalbugtracker/bugzilla.py
@@ -13,10 +13,10 @@ __all__ = [
 import re
 import xml.parsers.expat
 import xmlrpc.client
+from datetime import timezone
 from email.utils import parseaddr
 from http.client import BadStatusLine
 
-import pytz
 import requests
 from defusedxml import minidom
 from zope.component import getUtility
@@ -732,7 +732,7 @@ class BugzillaAPI(Bugzilla):
             # Bugzilla >= 5.1.1.  Times are always in UTC, so we can just
             # use db_time directly.
             server_utc_datetime = time_dict["db_time"]
-        return server_utc_datetime.replace(tzinfo=pytz.timezone("UTC"))
+        return server_utc_datetime.replace(tzinfo=timezone.utc)
 
     def _getActualBugId(self, bug_id):
         """Return the actual bug id for an alias or id."""
@@ -973,7 +973,7 @@ class BugzillaAPI(Bugzilla):
             owner=poster,
             subject="",
             content=comment["text"],
-            datecreated=comment["time"].replace(tzinfo=pytz.timezone("UTC")),
+            datecreated=comment["time"].replace(tzinfo=timezone.utc),
         )
 
     @ensure_no_transaction
@@ -1117,7 +1117,7 @@ class BugzillaLPPlugin(BugzillaAPI):
         # Return the UTC time sent by the server so that we don't have
         # to care about timezones.
         server_utc_time = time_dict["utc_time"]
-        return server_utc_time.replace(tzinfo=pytz.timezone("UTC"))
+        return server_utc_time.replace(tzinfo=timezone.utc)
 
     @ensure_no_transaction
     def getCommentIds(self, remote_bug_id):
diff --git a/lib/lp/bugs/externalbugtracker/debbugs.py b/lib/lp/bugs/externalbugtracker/debbugs.py
index 5bee46a..4cad41f 100644
--- a/lib/lp/bugs/externalbugtracker/debbugs.py
+++ b/lib/lp/bugs/externalbugtracker/debbugs.py
@@ -7,10 +7,9 @@ __all__ = ["DebBugs", "DebBugsDatabaseNotFound"]
 
 import email
 import os.path
-from datetime import datetime
+from datetime import datetime, timezone
 from email.utils import mktime_tz, parseaddr, parsedate_tz
 
-import pytz
 import transaction
 from zope.component import getUtility
 from zope.interface import implementer
@@ -97,7 +96,7 @@ class DebBugs(ExternalBugTracker):
         """See `IExternalBugTracker`."""
         # We don't know the exact time for the Debbugs server, but we
         # trust it being correct.
-        return datetime.now(pytz.timezone("UTC"))
+        return datetime.now(timezone.utc)
 
     def initializeRemoteBugDB(self, bug_ids):
         """See `ExternalBugTracker`.
@@ -312,9 +311,7 @@ class DebBugs(ExternalBugTracker):
         if date_string is not None:
             date_with_tz = parsedate_tz(date_string)
             timestamp = mktime_tz(date_with_tz)
-            msg_date = datetime.fromtimestamp(
-                timestamp, tz=pytz.timezone("UTC")
-            )
+            msg_date = datetime.fromtimestamp(timestamp, tz=timezone.utc)
         else:
             msg_date = None
 
diff --git a/lib/lp/bugs/externalbugtracker/github.py b/lib/lp/bugs/externalbugtracker/github.py
index 701ce14..02af254 100644
--- a/lib/lp/bugs/externalbugtracker/github.py
+++ b/lib/lp/bugs/externalbugtracker/github.py
@@ -12,9 +12,9 @@ __all__ = [
 import http.client
 import time
 from contextlib import contextmanager
+from datetime import timezone
 from urllib.parse import urlencode, urlunsplit
 
-import pytz
 import requests
 from zope.component import getUtility
 
@@ -155,7 +155,7 @@ class GitHub(ExternalBugTracker):
             return bugs
         params = [("state", "all")]
         if last_accessed is not None:
-            since = last_accessed.astimezone(pytz.UTC).strftime(
+            since = last_accessed.astimezone(timezone.utc).strftime(
                 "%Y-%m-%dT%H:%M:%SZ"
             )
             params.append(("since", since))
@@ -211,7 +211,7 @@ class GitHub(ExternalBugTracker):
             headers = {}
         if last_accessed is not None:
             headers["If-Modified-Since"] = last_accessed.astimezone(
-                pytz.UTC
+                timezone.utc
             ).strftime("%a, %d %b %Y %H:%M:%S GMT")
         with getUtility(IGitHubRateLimit).checkLimit(
             url, self.timeout, token=self.credentials["token"]
diff --git a/lib/lp/bugs/externalbugtracker/gitlab.py b/lib/lp/bugs/externalbugtracker/gitlab.py
index 67a29da..8f7a55f 100644
--- a/lib/lp/bugs/externalbugtracker/gitlab.py
+++ b/lib/lp/bugs/externalbugtracker/gitlab.py
@@ -9,10 +9,9 @@ __all__ = [
 ]
 
 import http.client
+from datetime import timezone
 from urllib.parse import quote, quote_plus, urlunsplit
 
-import pytz
-
 from lp.bugs.externalbugtracker import (
     BugTrackerConnectError,
     ExternalBugTracker,
@@ -88,7 +87,7 @@ class GitLab(ExternalBugTracker):
             return bugs
         params = []
         if last_accessed is not None:
-            since = last_accessed.astimezone(pytz.UTC).strftime(
+            since = last_accessed.astimezone(timezone.utc).strftime(
                 "%Y-%m-%dT%H:%M:%SZ"
             )
             params.append(("updated_after", since))
@@ -148,7 +147,7 @@ class GitLab(ExternalBugTracker):
             headers = {}
         if last_accessed is not None:
             headers["If-Modified-Since"] = last_accessed.astimezone(
-                pytz.UTC
+                timezone.utc
             ).strftime("%a, %d %b %Y %H:%M:%S GMT")
         token = self.credentials["token"]
         if token is not None:
diff --git a/lib/lp/bugs/externalbugtracker/tests/test_github.py b/lib/lp/bugs/externalbugtracker/tests/test_github.py
index a693f02..7ecc43b 100644
--- a/lib/lp/bugs/externalbugtracker/tests/test_github.py
+++ b/lib/lp/bugs/externalbugtracker/tests/test_github.py
@@ -4,10 +4,9 @@
 """Tests for the GitHub Issues BugTracker."""
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import parse_qs, urlsplit, urlunsplit
 
-import pytz
 import responses
 import transaction
 from testtools import ExpectedException
@@ -279,7 +278,7 @@ class TestGitHub(TestCase):
         _add_rate_limit_response("api.github.com")
         self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
-        since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)
+        since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
         self.assertEqual(
             {bug["number"]: bug for bug in self.sample_bugs[:2]},
             tracker.getRemoteBugBatch(["1", "2"], last_accessed=since),
diff --git a/lib/lp/bugs/externalbugtracker/tests/test_gitlab.py b/lib/lp/bugs/externalbugtracker/tests/test_gitlab.py
index d9ca160..a95b998 100644
--- a/lib/lp/bugs/externalbugtracker/tests/test_gitlab.py
+++ b/lib/lp/bugs/externalbugtracker/tests/test_gitlab.py
@@ -4,10 +4,9 @@
 """Tests for the GitLab Issues BugTracker."""
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import parse_qs, urlsplit, urlunsplit
 
-import pytz
 import responses
 import transaction
 from testtools.matchers import (
@@ -170,7 +169,7 @@ class TestGitLab(TestCase):
     def test_getRemoteBugBatch_last_accessed(self):
         self._addIssuesResponse()
         tracker = GitLab("https://gitlab.com/user/repository/issues";)
-        since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)
+        since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
         self.assertEqual(
             {bug["iid"]: bug for bug in self.sample_bugs[:2]},
             tracker.getRemoteBugBatch(["1", "2"], last_accessed=since),
diff --git a/lib/lp/bugs/externalbugtracker/trac.py b/lib/lp/bugs/externalbugtracker/trac.py
index 983296b..a37d990 100644
--- a/lib/lp/bugs/externalbugtracker/trac.py
+++ b/lib/lp/bugs/externalbugtracker/trac.py
@@ -9,10 +9,9 @@ import cgi
 import csv
 import time
 import xmlrpc.client
-from datetime import datetime
+from datetime import datetime, timezone
 from email.utils import parseaddr
 
-import pytz
 import requests
 from requests.cookies import RequestsCookieJar
 from zope.component import getUtility
@@ -411,7 +410,7 @@ class TracLPPlugin(Trac):
         # Return the UTC time, so we don't have to care about the time
         # zone for now.
         trac_time = datetime.utcfromtimestamp(utc_time)
-        return trac_time.replace(tzinfo=pytz.timezone("UTC"))
+        return trac_time.replace(tzinfo=timezone.utc)
 
     @ensure_no_transaction
     @needs_authentication
@@ -486,7 +485,7 @@ class TracLPPlugin(Trac):
         comment = bug["comments"][comment_id]
 
         comment_datecreated = datetime.fromtimestamp(
-            comment["timestamp"], pytz.timezone("UTC")
+            comment["timestamp"], timezone.utc
         )
         message = getUtility(IMessageSet).fromText(
             subject="",
diff --git a/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py b/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
index c499644..c6f63a3 100644
--- a/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
+++ b/lib/lp/bugs/mail/tests/test_bugnotificationbuilder.py
@@ -3,9 +3,8 @@
 
 """Tests for BugNotificationBuilder email construction."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.security.interfaces import Unauthorized
 
 from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
@@ -27,7 +26,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
 
     def test_build_filters_empty(self):
         """Filters are added."""
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         message = self.builder.build(
             "from", self.bug.owner, "body", "subject", utc_now, filters=[]
         )
@@ -35,7 +34,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
 
     def test_build_filters_single(self):
         """Filters are added."""
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         message = self.builder.build(
             "from",
             self.bug.owner,
@@ -50,7 +49,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
 
     def test_build_filters_multiple(self):
         """Filters are added."""
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         message = self.builder.build(
             "from",
             self.bug.owner,
@@ -65,7 +64,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
         )
 
     def test_mails_contain_notification_type_header(self):
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         message = self.builder.build(
             "from", self.bug.owner, "body", "subject", utc_now, filters=[]
         )
@@ -76,7 +75,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
     def test_mails_no_expanded_footer(self):
         # Recipients without expanded_notification_footers do not receive an
         # expanded footer on messages.
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         message = self.builder.build(
             "from", self.bug.owner, "body", "subject", utc_now, filters=[]
         )
@@ -87,7 +86,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
     def test_mails_append_expanded_footer(self):
         # Recipients with expanded_notification_footers receive an expanded
         # footer on messages.
-        utc_now = datetime.now(pytz.UTC)
+        utc_now = datetime.now(timezone.utc)
         with person_logged_in(self.bug.owner):
             self.bug.owner.expanded_notification_footers = True
         message = self.builder.build(
@@ -113,7 +112,7 @@ class TestBugNotificationBuilder(TestCaseWithFactory):
                 private_team,
                 "expanded_notification_footers",
             )
-            utc_now = datetime.now(pytz.UTC)
+            utc_now = datetime.now(timezone.utc)
             message = self.builder.build(
                 "from", private_team, "body", "subject", utc_now, filters=[]
             )
diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
index e9a6304..5f20232 100644
--- a/lib/lp/bugs/model/bug.py
+++ b/lib/lp/bugs/model/bug.py
@@ -22,12 +22,12 @@ import http.client
 import operator
 import re
 from collections.abc import Iterable, Set
+from datetime import timezone
 from email.utils import make_msgid
 from functools import wraps
 from io import BytesIO
 from itertools import chain
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restful.declarations import error_status
@@ -3477,7 +3477,7 @@ class BugMute(StormBase):
     __storm_primary__ = "person_id", "bug_id"
 
     date_created = DateTime(
-        "date_created", allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC
+        "date_created", allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
     )
 
 
diff --git a/lib/lp/bugs/model/bugactivity.py b/lib/lp/bugs/model/bugactivity.py
index 899aec3..2ff26bb 100644
--- a/lib/lp/bugs/model/bugactivity.py
+++ b/lib/lp/bugs/model/bugactivity.py
@@ -4,8 +4,8 @@
 __all__ = ["BugActivity", "BugActivitySet"]
 
 import re
+from datetime import timezone
 
-import pytz
 from storm.locals import DateTime, Int, Reference, Unicode
 from storm.store import Store
 from zope.interface import implementer
@@ -42,7 +42,7 @@ class BugActivity(StormBase):
     bug_id = Int(name="bug", allow_none=False)
     bug = Reference(bug_id, "Bug.id")
 
-    datechanged = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    datechanged = DateTime(tzinfo=timezone.utc, allow_none=False)
 
     person_id = Int(name="person", allow_none=False, validator=validate_person)
     person = Reference(person_id, "Person.id")
diff --git a/lib/lp/bugs/model/bugbranch.py b/lib/lp/bugs/model/bugbranch.py
index 3da6f5b..4f0cdd4 100644
--- a/lib/lp/bugs/model/bugbranch.py
+++ b/lib/lp/bugs/model/bugbranch.py
@@ -8,7 +8,8 @@ __all__ = [
     "BugBranchSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Store
 from zope.interface import implementer
 
@@ -28,7 +29,10 @@ class BugBranch(StormBase):
     id = Int(primary=True)
 
     datecreated = DateTime(
-        name="datecreated", tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW
+        name="datecreated",
+        tzinfo=timezone.utc,
+        allow_none=False,
+        default=UTC_NOW,
     )
     bug_id = Int(name="bug", allow_none=False)
     bug = Reference(bug_id, "Bug.id")
diff --git a/lib/lp/bugs/model/bugnomination.py b/lib/lp/bugs/model/bugnomination.py
index c89e570..0d3846c 100644
--- a/lib/lp/bugs/model/bugnomination.py
+++ b/lib/lp/bugs/model/bugnomination.py
@@ -10,9 +10,8 @@ or more nominations.
 
 __all__ = ["BugNomination", "BugNominationSet"]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.properties import DateTime, Int
 from storm.references import Reference
 from zope.component import getUtility
@@ -58,8 +57,10 @@ class BugNomination(StormBase):
     )
     decider = Reference(decider_id, "Person.id")
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
-    date_decided = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
+    date_decided = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
 
     distroseries_id = Int(name="distroseries", allow_none=True, default=None)
     distroseries = Reference(distroseries_id, "DistroSeries.id")
@@ -109,7 +110,7 @@ class BugNomination(StormBase):
             return
         self.status = BugNominationStatus.APPROVED
         self.decider = approver
-        self.date_decided = datetime.now(pytz.timezone("UTC"))
+        self.date_decided = datetime.now(timezone.utc)
         targets = []
         if self.distroseries:
             # Figure out which packages are affected in this distro for
@@ -141,7 +142,7 @@ class BugNomination(StormBase):
             )
         self.status = BugNominationStatus.DECLINED
         self.decider = decliner
-        self.date_decided = datetime.now(pytz.timezone("UTC"))
+        self.date_decided = datetime.now(timezone.utc)
 
     def isProposed(self):
         """See IBugNomination."""
diff --git a/lib/lp/bugs/model/bugnotification.py b/lib/lp/bugs/model/bugnotification.py
index f2476ec..9634c6a 100644
--- a/lib/lp/bugs/model/bugnotification.py
+++ b/lib/lp/bugs/model/bugnotification.py
@@ -10,9 +10,8 @@ __all__ = [
     "BugNotificationSet",
 ]
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.expr import In, Join, LeftJoin
 from storm.locals import Bool, DateTime, Int, Reference, Unicode
 from storm.store import Store
@@ -59,7 +58,7 @@ class BugNotification(StormBase):
     bug = Reference(bug_id, "Bug.id")
 
     is_comment = Bool(allow_none=False)
-    date_emailed = DateTime(tzinfo=pytz.UTC, allow_none=True)
+    date_emailed = DateTime(tzinfo=timezone.utc, allow_none=True)
     status = DBEnum(
         name="status",
         enum=BugNotificationStatus,
@@ -141,7 +140,7 @@ class BugNotificationSet:
         interval = timedelta(
             minutes=int(config.malone.bugnotification_interval)
         )
-        time_limit = datetime.now(pytz.UTC) - interval
+        time_limit = datetime.now(timezone.utc) - interval
         last_omitted_notification = None
         pending_notifications = []
         people_ids = set()
diff --git a/lib/lp/bugs/model/bugsubscription.py b/lib/lp/bugs/model/bugsubscription.py
index a0efed7..95f7b6e 100644
--- a/lib/lp/bugs/model/bugsubscription.py
+++ b/lib/lp/bugs/model/bugsubscription.py
@@ -3,7 +3,8 @@
 
 __all__ = ["BugSubscription"]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference
 from zope.interface import implementer
 
@@ -36,7 +37,9 @@ class BugSubscription(StormBase):
         allow_none=False,
     )
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     subscribed_by_id = Int(
         "subscribed_by", allow_none=False, validator=validate_person
diff --git a/lib/lp/bugs/model/bugsubscriptionfilter.py b/lib/lp/bugs/model/bugsubscriptionfilter.py
index 51bfa2f..49b1c3e 100644
--- a/lib/lp/bugs/model/bugsubscriptionfilter.py
+++ b/lib/lp/bugs/model/bugsubscriptionfilter.py
@@ -11,9 +11,9 @@ __all__ = [
 ]
 
 import http.client
+from datetime import timezone
 from itertools import chain
 
-import pytz
 from lazr.restful.declarations import error_status
 from storm.expr import SQL, Exists, Not, Select
 from storm.properties import Bool, DateTime, Int, Unicode
@@ -370,7 +370,7 @@ class BugSubscriptionFilterMute(StormBase):
     __storm_primary__ = "person_id", "filter_id"
 
     date_created = DateTime(
-        "date_created", allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC
+        "date_created", allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
     )
 
 
diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
index ed816e8..2a0dce7 100644
--- a/lib/lp/bugs/model/bugtask.py
+++ b/lib/lp/bugs/model/bugtask.py
@@ -16,13 +16,12 @@ __all__ = [
 ]
 
 
-import datetime
 import re
 from collections import defaultdict
+from datetime import datetime, timezone
 from itertools import chain, repeat
 from operator import attrgetter, itemgetter
 
-import pytz
 from lazr.lifecycle.event import ObjectDeletedEvent
 from storm.expr import (
     SQL,
@@ -535,62 +534,64 @@ class BugTask(StormBase):
     bugwatch = Reference(bugwatch_id, "BugWatch.id")
 
     date_assigned = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
-    datecreated = DateTime(tzinfo=pytz.UTC, allow_none=True, default=UTC_NOW)
+    datecreated = DateTime(
+        tzinfo=timezone.utc, allow_none=True, default=UTC_NOW
+    )
     date_confirmed = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_inprogress = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_closed = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_incomplete = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_left_new = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_triaged = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_fix_committed = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_fix_released = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
     )
     date_left_closed = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         allow_none=True,
         default=None,
         validator=validate_conjoined_attribute,
@@ -683,7 +684,7 @@ class BugTask(StormBase):
     @property
     def age(self):
         """See `IBugTask`."""
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         return now - self.datecreated
 
@@ -1073,7 +1074,7 @@ class BugTask(StormBase):
             return
 
         if when is None:
-            when = datetime.datetime.now(pytz.UTC)
+            when = datetime.now(timezone.utc)
 
         # Record the date of the particular kinds of transitions into
         # certain states.
@@ -1221,7 +1222,7 @@ class BugTask(StormBase):
         if not self.assignee and assignee:
             # The task is going from not having an assignee to having
             # one, so record when this happened
-            self.date_assigned = datetime.datetime.now(pytz.UTC)
+            self.date_assigned = datetime.now(timezone.utc)
 
         self.assignee = assignee
 
diff --git a/lib/lp/bugs/model/bugtracker.py b/lib/lp/bugs/model/bugtracker.py
index 4fb96be..6197847 100644
--- a/lib/lp/bugs/model/bugtracker.py
+++ b/lib/lp/bugs/model/bugtracker.py
@@ -10,13 +10,12 @@ __all__ = [
     "BugTrackerSet",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 from itertools import chain
 from operator import itemgetter
 from urllib.parse import quote, urlsplit, urlunsplit
 
 from lazr.uri import URI
-from pytz import timezone
 from storm.expr import Count, Desc, Not, Or
 from storm.locals import SQL, Bool, Int, Reference, ReferenceSet, Unicode
 from storm.store import Store
@@ -579,7 +578,7 @@ class BugTracker(StormBase):
             BugWatch,
             BugWatch.bugtracker == self,
             Not(BugWatch.next_check == None),
-            BugWatch.next_check <= datetime.now(timezone("UTC")),
+            BugWatch.next_check <= datetime.now(timezone.utc),
         )
 
     @property
diff --git a/lib/lp/bugs/model/bugtrackerperson.py b/lib/lp/bugs/model/bugtrackerperson.py
index 78fd7f8..f5d6eeb 100644
--- a/lib/lp/bugs/model/bugtrackerperson.py
+++ b/lib/lp/bugs/model/bugtrackerperson.py
@@ -7,7 +7,8 @@ __all__ = [
     "BugTrackerPerson",
 ]
 
-import pytz
+from datetime import timezone
+
 import six
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
@@ -33,7 +34,10 @@ class BugTrackerPerson(StormBase):
     name = Unicode(allow_none=False)
 
     date_created = DateTime(
-        tzinfo=pytz.UTC, name="date_created", allow_none=False, default=UTC_NOW
+        tzinfo=timezone.utc,
+        name="date_created",
+        allow_none=False,
+        default=UTC_NOW,
     )
 
     def __init__(self, name, bugtracker, person):
diff --git a/lib/lp/bugs/model/bugwatch.py b/lib/lp/bugs/model/bugwatch.py
index 9732f16..429ae44 100644
--- a/lib/lp/bugs/model/bugwatch.py
+++ b/lib/lp/bugs/model/bugwatch.py
@@ -9,14 +9,13 @@ __all__ = [
 ]
 
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import urlunsplit
 
 import six
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.uri import find_uris_in_text
-from pytz import utc
 from storm.expr import Desc, Not
 from storm.locals import DateTime, Int, Reference, Unicode
 from storm.store import Store
@@ -105,15 +104,17 @@ class BugWatch(StormBase):
     remotebug = Unicode(allow_none=False)
     remotestatus = Unicode(allow_none=True, default=None)
     remote_importance = Unicode(allow_none=True, default=None)
-    lastchanged = DateTime(allow_none=True, default=None, tzinfo=utc)
-    lastchecked = DateTime(allow_none=True, default=None, tzinfo=utc)
+    lastchanged = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
+    lastchecked = DateTime(allow_none=True, default=None, tzinfo=timezone.utc)
     last_error_type = DBEnum(enum=BugWatchActivityStatus, default=None)
-    datecreated = DateTime(allow_none=False, default=UTC_NOW, tzinfo=utc)
+    datecreated = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
     owner_id = Int(
         name="owner", validator=validate_public_person, allow_none=False
     )
     owner = Reference(owner_id, "Person.id")
-    next_check = DateTime(tzinfo=utc)
+    next_check = DateTime(tzinfo=timezone.utc)
 
     def __init__(
         self,
@@ -341,7 +342,7 @@ class BugWatch(StormBase):
     def can_be_rescheduled(self):
         """See `IBugWatch`."""
         if self.next_check is not None and self.next_check <= datetime.now(
-            utc
+            timezone.utc
         ):
             # If the watch is already scheduled for a time in the past
             # (or for right now) it can't be rescheduled, since it
@@ -840,7 +841,7 @@ class BugWatchActivity(StormBase):
     id = Int(primary=True)
     bug_watch_id = Int(name="bug_watch")
     bug_watch = Reference(bug_watch_id, BugWatch.id)
-    activity_date = DateTime(allow_none=False, tzinfo=utc)
+    activity_date = DateTime(allow_none=False, tzinfo=timezone.utc)
     result = DBEnum(enum=BugWatchActivityStatus, allow_none=True)
     message = Unicode()
     oops_id = Unicode()
diff --git a/lib/lp/bugs/model/cve.py b/lib/lp/bugs/model/cve.py
index 0cd8e0e..dd19e0d 100644
--- a/lib/lp/bugs/model/cve.py
+++ b/lib/lp/bugs/model/cve.py
@@ -7,8 +7,8 @@ __all__ = [
 ]
 
 import operator
+from datetime import timezone
 
-import pytz
 from storm.databases.postgres import JSON
 from storm.locals import DateTime, Desc, Int, ReferenceSet, Store, Unicode
 from zope.component import getUtility
@@ -48,14 +48,18 @@ class Cve(StormBase, BugLinkTargetMixin):
     sequence = Unicode(allow_none=False)
     status = DBEnum(name="status", enum=CveStatus, allow_none=False)
     description = Unicode(allow_none=False)
-    datecreated = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
-    datemodified = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    datecreated = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    datemodified = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     references = ReferenceSet(
         id, "CveReference.cve_id", order_by="CveReference.id"
     )
 
-    date_made_public = DateTime(tzinfo=pytz.UTC, allow_none=True)
+    date_made_public = DateTime(tzinfo=timezone.utc, allow_none=True)
     discovered_by = Unicode(allow_none=True)
     _cvss = JSON(name="cvss", allow_none=True)
 
diff --git a/lib/lp/bugs/model/structuralsubscription.py b/lib/lp/bugs/model/structuralsubscription.py
index a5c9519..75b40e6 100644
--- a/lib/lp/bugs/model/structuralsubscription.py
+++ b/lib/lp/bugs/model/structuralsubscription.py
@@ -12,8 +12,8 @@ __all__ = [
 ]
 
 from collections import defaultdict
+from datetime import timezone
 
-import pytz
 from storm.expr import (
     SQL,
     And,
@@ -121,10 +121,13 @@ class StructuralSubscription(StormBase):
     subscribed_by = Reference(subscribed_byID, "Person.id")
 
     date_created = DateTime(
-        "date_created", allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC
+        "date_created", allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
     )
     date_last_updated = DateTime(
-        "date_last_updated", allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC
+        "date_last_updated",
+        allow_none=False,
+        default=UTC_NOW,
+        tzinfo=timezone.utc,
     )
 
     def __init__(self, subscriber, subscribed_by, **kwargs):
diff --git a/lib/lp/bugs/model/tests/test_bug.py b/lib/lp/bugs/model/tests/test_bug.py
index 4f708c4..1084734 100644
--- a/lib/lp/bugs/model/tests/test_bug.py
+++ b/lib/lp/bugs/model/tests/test_bug.py
@@ -1,10 +1,9 @@
 # Copyright 2010-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 from lazr.lifecycle.event import ObjectCreatedEvent
-from pytz import UTC
 from storm.store import Store
 from testtools.testcase import ExpectedException
 from zope.component import getUtility
@@ -1095,7 +1094,7 @@ class TestBugActivityMethods(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.now = datetime.now(UTC)
+        self.now = datetime.now(timezone.utc)
 
     def _makeActivityForBug(self, bug, activity_ages):
         with person_logged_in(bug.owner):
diff --git a/lib/lp/bugs/model/tests/test_bugsummary.py b/lib/lp/bugs/model/tests/test_bugsummary.py
index f231c35..459a6f0 100644
--- a/lib/lp/bugs/model/tests/test_bugsummary.py
+++ b/lib/lp/bugs/model/tests/test_bugsummary.py
@@ -3,9 +3,8 @@
 
 """Tests for the BugSummary class and underlying database triggers."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import utc
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -634,14 +633,18 @@ class TestBugSummary(TestCaseWithFactory):
 
         self.assertCount(0, product=product, has_patch=True)
 
-        removeSecurityProxy(bug).latest_patch_uploaded = datetime.now(tz=utc)
+        removeSecurityProxy(bug).latest_patch_uploaded = datetime.now(
+            tz=timezone.utc
+        )
 
         self.assertCount(1, product=product, has_patch=True)
 
     def test_removePatch(self):
         product = self.factory.makeProduct()
         bug = self.factory.makeBug(target=product)
-        removeSecurityProxy(bug).latest_patch_uploaded = datetime.now(tz=utc)
+        removeSecurityProxy(bug).latest_patch_uploaded = datetime.now(
+            tz=timezone.utc
+        )
 
         self.assertCount(1, product=product, has_patch=True)
         self.assertCount(0, product=product, has_patch=False)
diff --git a/lib/lp/bugs/model/tests/test_bugtasksearch.py b/lib/lp/bugs/model/tests/test_bugtasksearch.py
index 05793e0..918dbdd 100644
--- a/lib/lp/bugs/model/tests/test_bugtasksearch.py
+++ b/lib/lp/bugs/model/tests/test_bugtasksearch.py
@@ -2,10 +2,9 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import unittest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-import pytz
 from storm.expr import Or
 from testtools.matchers import Equals
 from testtools.testcase import ExpectedException
@@ -366,7 +365,7 @@ class OnceTests:
             self.bugtasks[2].transitionToStatus(
                 BugTaskStatus.FIXRELEASED, self.owner
             )
-        utc_now = datetime.now(pytz.timezone("UTC"))
+        utc_now = datetime.now(timezone.utc)
         self.assertTrue(utc_now >= self.bugtasks[2].date_closed)
         params = self.getBugTaskSearchParams(
             user=None, date_closed=greater_than(utc_now - timedelta(days=1))
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
index 53eef9d..c4c4a91 100644
--- a/lib/lp/bugs/model/vulnerability.py
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -8,9 +8,9 @@ __all__ = [
 ]
 
 import operator
+from datetime import timezone
 from typing import Iterable
 
-import pytz
 from storm.expr import SQL, Coalesce, Join, Or, Select
 from storm.locals import DateTime, Int, Reference, Unicode
 from storm.store import Store
@@ -93,17 +93,20 @@ class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
     )
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW
+        name="date_created",
+        tzinfo=timezone.utc,
+        allow_none=False,
+        default=UTC_NOW,
     )
 
     date_made_public = DateTime(
-        name="date_made_public", tzinfo=pytz.UTC, allow_none=True
+        name="date_made_public", tzinfo=timezone.utc, allow_none=True
     )
     date_notice_issued = DateTime(
-        name="date_notice_issued", tzinfo=pytz.UTC, allow_none=True
+        name="date_notice_issued", tzinfo=timezone.utc, allow_none=True
     )
     date_coordinated_release = DateTime(
-        name="date_coordinated_release", tzinfo=pytz.UTC, allow_none=True
+        name="date_coordinated_release", tzinfo=timezone.utc, allow_none=True
     )
 
     creator_id = Int(name="creator", allow_none=False)
@@ -382,7 +385,7 @@ class VulnerabilityActivity(StormBase):
     changer = Reference(changer_id, "Person.id")
 
     date_changed = DateTime(
-        name="date_changed", tzinfo=pytz.UTC, allow_none=False
+        name="date_changed", tzinfo=timezone.utc, allow_none=False
     )
 
     what_changed = DBEnum(
diff --git a/lib/lp/bugs/model/vulnerabilitysubscription.py b/lib/lp/bugs/model/vulnerabilitysubscription.py
index e6faf0f..c25d448 100644
--- a/lib/lp/bugs/model/vulnerabilitysubscription.py
+++ b/lib/lp/bugs/model/vulnerabilitysubscription.py
@@ -5,7 +5,8 @@
 
 __all__ = ["VulnerabilitySubscription"]
 
-import pytz
+from datetime import timezone
+
 from storm.properties import DateTime, Int
 from storm.references import Reference
 from zope.interface import implementer
@@ -34,7 +35,9 @@ class VulnerabilitySubscription(StormBase):
     vulnerability_id = Int("vulnerability", allow_none=False)
     vulnerability = Reference(vulnerability_id, "Vulnerability.id")
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     subscribed_by_id = Int(
         "subscribed_by", allow_none=False, validator=validate_person
diff --git a/lib/lp/bugs/scripts/bugimport.py b/lib/lp/bugs/scripts/bugimport.py
index 38306fb..4618c72 100644
--- a/lib/lp/bugs/scripts/bugimport.py
+++ b/lib/lp/bugs/scripts/bugimport.py
@@ -13,14 +13,13 @@ __all__ = [
 ]
 
 import base64
-import datetime
 import io
 import logging
 import os
 import pickle
 import time
+from datetime import datetime, timezone
 
-import pytz
 import six
 from defusedxml import cElementTree
 from storm.store import Store
@@ -49,8 +48,6 @@ from lp.services.messages.interfaces.message import IMessageSet
 
 DEFAULT_LOGGER = logging.getLogger("lp.bugs.scripts.bugimport")
 
-UTC = pytz.timezone("UTC")
-
 
 class BugXMLSyntaxError(Exception):
     """A syntax error was detected in the input."""
@@ -63,7 +60,7 @@ def parse_date(datestr):
     year, month, day, hour, minute, second = time.strptime(
         datestr, "%Y-%m-%dT%H:%M:%SZ"
     )[:6]
-    return datetime.datetime(year, month, day, hour, minute, tzinfo=UTC)
+    return datetime(year, month, day, hour, minute, tzinfo=timezone.utc)
 
 
 def get_text(node):
diff --git a/lib/lp/bugs/scripts/checkwatches/core.py b/lib/lp/bugs/scripts/checkwatches/core.py
index 43850c2..63a4546 100644
--- a/lib/lp/bugs/scripts/checkwatches/core.py
+++ b/lib/lp/bugs/scripts/checkwatches/core.py
@@ -19,12 +19,11 @@ import threading
 import time
 from contextlib import contextmanager
 from copy import copy
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from itertools import chain, islice
 from typing import List
 from xmlrpc.client import ProtocolError
 
-import pytz
 from twisted.internet import reactor
 from twisted.internet.defer import DeferredList
 from twisted.internet.threads import deferToThreadPool
@@ -328,9 +327,7 @@ class CheckwatchesMaster(WorkingBase):
                 "Resetting %s bug watches for bug tracker '%s'"
                 % (bug_tracker.watches.count(), bug_tracker_name)
             )
-            bug_tracker.resetWatches(
-                new_next_check=datetime.now(pytz.timezone("UTC"))
-            )
+            bug_tracker.resetWatches(new_next_check=datetime.now(timezone.utc))
 
         # Loop over the bug watches in batches as specified by
         # batch_size until there are none left to update.
@@ -493,7 +490,7 @@ class CheckwatchesMaster(WorkingBase):
         # server's wrong about the time it'll mess up all our times when
         # we import things.
         if now is None:
-            now = datetime.now(pytz.timezone("UTC"))
+            now = datetime.now(timezone.utc)
 
         if (
             server_time is not None
diff --git a/lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py b/lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py
index cdaa2fc..e2b256d 100644
--- a/lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py
+++ b/lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py
@@ -3,10 +3,9 @@
 
 """XXX: Module docstring goes here."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import transaction
-from pytz import utc
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -27,7 +26,7 @@ class TestBugWatchScheduler(TestCaseWithFactory):
         # We'll make sure that all the other bug watches look like
         # they've been scheduled so that only our watch gets scheduled.
         for watch in getUtility(IBugWatchSet).search():
-            removeSecurityProxy(watch).next_check = datetime.now(utc)
+            removeSecurityProxy(watch).next_check = datetime.now(timezone.utc)
         self.bug_watch = removeSecurityProxy(self.factory.makeBugWatch())
         self.scheduler = BugWatchScheduler(BufferLogger())
         transaction.commit()
@@ -39,12 +38,14 @@ class TestBugWatchScheduler(TestCaseWithFactory):
         self.scheduler(1)
 
         self.assertNotEqual(None, self.bug_watch.next_check)
-        self.assertTrue(self.bug_watch.next_check <= datetime.now(utc))
+        self.assertTrue(
+            self.bug_watch.next_check <= datetime.now(timezone.utc)
+        )
 
     def test_scheduler_schedules_working_watches(self):
         # If a watch has been checked and has never failed its next
         # check will be scheduled for 24 hours after its last check.
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         # Add some successful activity to ensure that successful activity
         # is handled correctly.
         self.bug_watch.addActivity()
@@ -58,7 +59,7 @@ class TestBugWatchScheduler(TestCaseWithFactory):
     def test_scheduler_schedules_failing_watches(self):
         # If a watch has failed once, it will be scheduled more than 24
         # hours after its last check.
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         self.bug_watch.lastchecked = now
 
         # The delay depends on the number of failures that the watch has
@@ -92,7 +93,7 @@ class TestBugWatchScheduler(TestCaseWithFactory):
     def test_scheduler_doesnt_schedule_scheduled_watches(self):
         # The scheduler will ignore watches whose next_check has been
         # set.
-        next_check_date = datetime.now(utc) + timedelta(days=1)
+        next_check_date = datetime.now(timezone.utc) + timedelta(days=1)
         self.bug_watch.next_check = next_check_date
         transaction.commit()
         self.scheduler(1)
diff --git a/lib/lp/bugs/scripts/tests/test_bugimport.py b/lib/lp/bugs/scripts/tests/test_bugimport.py
index 738b9c3..f091afb 100644
--- a/lib/lp/bugs/scripts/tests/test_bugimport.py
+++ b/lib/lp/bugs/scripts/tests/test_bugimport.py
@@ -3,9 +3,9 @@
 
 import os
 import re
+from datetime import timezone
 
 import defusedxml.cElementTree as ET
-import pytz
 import transaction
 from testtools.content import text_content
 from zope.component import getUtility
@@ -55,7 +55,7 @@ class UtilsTestCase(TestCase):
         self.assertEqual(dt.hour, 8)
         self.assertEqual(dt.minute, 0)
         self.assertEqual(dt.second, 0)
-        self.assertEqual(dt.tzinfo, pytz.timezone("UTC"))
+        self.assertEqual(dt.tzinfo, timezone.utc)
 
     def test_get_text(self):
         # Test that the get_text() helper can correctly return the
diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
index 80fee9c..9dc39fb 100644
--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
@@ -4,11 +4,10 @@
 
 import re
 import unittest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from smtplib import SMTPException
 from typing import Any, List, Optional, Type
 
-import pytz
 from fixtures import FakeLogger
 from testtools.matchers import MatchesRegex, Not
 from transaction import commit
@@ -241,7 +240,7 @@ class TestGetEmailNotifications(TestCase):
         super().setUp()
         switch_dbuser(config.malone.bugnotification_dbuser)
         sample_person = getUtility(IPersonSet).getByEmail("test@xxxxxxxxxxxxx")
-        self.now = datetime.now(pytz.timezone("UTC"))
+        self.now = datetime.now(timezone.utc)
 
         # A normal comment notification for bug 1
         msg = getUtility(IMessageSet).fromText(
@@ -700,7 +699,7 @@ class EmailNotificationTestBase(TestCaseWithFactory):
         commit()
         login("test@xxxxxxxxxxxxx")
         switch_dbuser(config.malone.bugnotification_dbuser)
-        self.now = datetime.now(pytz.UTC)
+        self.now = datetime.now(timezone.utc)
         self.ten_minutes_ago = self.now - timedelta(minutes=10)
         self.notification_set = getUtility(IBugNotificationSet)
         for notification in self.notification_set.getNotificationsToSend():
@@ -1401,7 +1400,9 @@ class TestDeferredNotifications(TestCaseWithFactory):
         # Ensure there are no outstanding notifications.
         for notification in self.notification_set.getNotificationsToSend():
             notification.destroySelf()
-        self.ten_minutes_ago = datetime.now(pytz.UTC) - timedelta(minutes=10)
+        self.ten_minutes_ago = datetime.now(timezone.utc) - timedelta(
+            minutes=10
+        )
 
     def _make_deferred_notification(self):
         bug = self.factory.makeBug()
@@ -1455,7 +1456,9 @@ class TestSendBugNotifications(TestCaseWithFactory):
         # Ensure there are no outstanding notifications.
         for notification in self.notification_set.getNotificationsToSend():
             notification.destroySelf()
-        self.ten_minutes_ago = datetime.now(pytz.UTC) - timedelta(minutes=10)
+        self.ten_minutes_ago = datetime.now(timezone.utc) - timedelta(
+            minutes=10
+        )
 
     def test_oops_on_failed_delivery(self):
         # If one notification fails to send, it logs an OOPS and doesn't get
diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py
index 9813d86..9837d3c 100644
--- a/lib/lp/bugs/scripts/tests/test_uct.py
+++ b/lib/lp/bugs/scripts/tests/test_uct.py
@@ -1,10 +1,10 @@
 #  Copyright 2022 Canonical Ltd.  This software is licensed under the
 #  GNU Affero General Public License version 3 (see the file LICENSE).
-import datetime
+
+from datetime import datetime, timezone
 from pathlib import Path
 from typing import List
 
-from pytz import UTC
 from zope.component import getUtility
 
 from lp.app.enums import InformationType
@@ -56,12 +56,10 @@ class TestUCTRecord(TestCase):
                 ],
                 candidate="CVE-2022-23222",
                 crd=None,
-                public_date_at_USN=datetime.datetime(
-                    2022, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
-                ),
-                public_date=datetime.datetime(
-                    2022, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+                public_date_at_USN=datetime(
+                    2022, 1, 14, 8, 15, tzinfo=timezone.utc
                 ),
+                public_date=datetime(2022, 1, 14, 8, 15, tzinfo=timezone.utc),
                 description=(
                     "kernel/bpf/verifier.c in the Linux kernel through "
                     "5.15.14 allows local\nusers to gain privileges because "
@@ -225,15 +223,11 @@ class TestCVE(TestCaseWithFactory):
                 ),
             ],
             candidate="CVE-2022-23222",
-            crd=datetime.datetime(
-                2020, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
-            ),
-            public_date_at_USN=datetime.datetime(
-                2021, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
-            ),
-            public_date=datetime.datetime(
-                2022, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+            crd=datetime(2020, 1, 14, 8, 15, tzinfo=timezone.utc),
+            public_date_at_USN=datetime(
+                2021, 1, 14, 8, 15, tzinfo=timezone.utc
             ),
+            public_date=datetime(2022, 1, 14, 8, 15, tzinfo=timezone.utc),
             description="description",
             discovered_by="tr3e wang",
             mitigation="mitigation",
@@ -326,14 +320,12 @@ class TestCVE(TestCaseWithFactory):
 
         self.cve = CVE(
             sequence="CVE-2022-23222",
-            date_made_public=datetime.datetime(
-                2022, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
-            ),
-            date_notice_issued=datetime.datetime(
-                2021, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+            date_made_public=datetime(2022, 1, 14, 8, 15, tzinfo=timezone.utc),
+            date_notice_issued=datetime(
+                2021, 1, 14, 8, 15, tzinfo=timezone.utc
             ),
-            date_coordinated_release=datetime.datetime(
-                2020, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+            date_coordinated_release=datetime(
+                2020, 1, 14, 8, 15, tzinfo=timezone.utc
             ),
             distro_packages=[
                 CVE.DistroPackage(
@@ -581,14 +573,12 @@ class TestUCTImporterExporter(TestCaseWithFactory):
         self.lp_cve = self.factory.makeCVE("2022-23222")
         self.cve = CVE(
             sequence="CVE-2022-23222",
-            date_made_public=datetime.datetime(
-                2022, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+            date_made_public=datetime(2022, 1, 14, 8, 15, tzinfo=timezone.utc),
+            date_notice_issued=datetime(
+                2021, 1, 14, 8, 15, tzinfo=timezone.utc
             ),
-            date_notice_issued=datetime.datetime(
-                2021, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
-            ),
-            date_coordinated_release=datetime.datetime(
-                2020, 1, 14, 8, 15, tzinfo=datetime.timezone.utc
+            date_coordinated_release=datetime(
+                2020, 1, 14, 8, 15, tzinfo=timezone.utc
             ),
             distro_packages=[
                 CVE.DistroPackage(
@@ -1242,14 +1232,14 @@ class TestUCTImporterExporter(TestCaseWithFactory):
             bug.vulnerabilities[0].date_notice_issued,
             bug.vulnerabilities[0].date_coordinated_release,
         ):
-            self.assertEqual(UTC, date.tzinfo)
+            self.assertEqual(timezone.utc, date.tzinfo)
         self.importer.update_bug(bug, cve, self.lp_cve)
         for date in (
             bug.vulnerabilities[0].date_made_public,
             bug.vulnerabilities[0].date_notice_issued,
             bug.vulnerabilities[0].date_coordinated_release,
         ):
-            self.assertEqual(UTC, date.tzinfo)
+            self.assertEqual(timezone.utc, date.tzinfo)
 
     def test_make_cve_from_bug(self):
         self.importer.import_cve(self.cve)
diff --git a/lib/lp/bugs/stories/bugtracker/xx-bugtracker.rst b/lib/lp/bugs/stories/bugtracker/xx-bugtracker.rst
index 9367df3..0beb941 100644
--- a/lib/lp/bugs/stories/bugtracker/xx-bugtracker.rst
+++ b/lib/lp/bugs/stories/bugtracker/xx-bugtracker.rst
@@ -647,15 +647,14 @@ When looking at a bug tracker page, a list of bug watches is displayed:
 
 Scheduling any of the watches will change their "Next check" column.
 
-    >>> from datetime import datetime
-    >>> from pytz import utc
+    >>> from datetime import datetime, timezone
     >>> from zope.security.proxy import removeSecurityProxy
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> debbugs = getUtility(IBugTrackerSet).getByName("debbugs")
     >>> watch_15 = debbugs.watches[0]
     >>> removeSecurityProxy(watch_15).next_check = datetime(
-    ...     2010, 4, 9, 9, 50, 0, tzinfo=utc
+    ...     2010, 4, 9, 9, 50, 0, tzinfo=timezone.utc
     ... )
     >>> logout()
 
diff --git a/lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.rst b/lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.rst
index 7dee1e3..5ee39c7 100644
--- a/lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.rst
+++ b/lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.rst
@@ -65,11 +65,10 @@ checking at some future date.
 If we look at the bug watch on our bugtracker we can see that it has
 been scheduled for checking at some point in the future.
 
-    >>> from datetime import datetime
-    >>> from pytz import utc
+    >>> from datetime import datetime, timezone
 
     >>> login(ADMIN_EMAIL)
-    >>> print(bug_watch.next_check >= datetime.now(utc))
+    >>> print(bug_watch.next_check >= datetime.now(timezone.utc))
     True
 
 Should the bug watch be deleted the reschedule button will no longer
diff --git a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.rst b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.rst
index a030f1e..4ef9680 100644
--- a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.rst
+++ b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.rst
@@ -67,8 +67,7 @@ If we change the next_check date of the watch its will be shown in the
 Next check column.
 
     >>> from zope.component import getUtility
-    >>> from datetime import datetime
-    >>> from pytz import utc
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.security.proxy import removeSecurityProxy
 
     >>> from lp.testing import login, logout
@@ -77,7 +76,7 @@ Next check column.
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> watch = getUtility(IBugWatchSet).get(2)
     >>> removeSecurityProxy(watch).next_check = datetime(
-    ...     2010, 4, 8, 16, 7, tzinfo=utc
+    ...     2010, 4, 8, 16, 7, tzinfo=timezone.utc
     ... )
     >>> logout()
 
@@ -273,11 +272,9 @@ the watch has failed > 60% of the time. This is because the most recent
 check succeeded, so there's no point in allowing users to reschedule the
 watch for checking.
 
-    >>> from datetime import timedelta
-
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> removeSecurityProxy(bug_watch).next_check = datetime.now(
-    ...     utc
+    ...     timezone.utc
     ... ) + timedelta(days=7)
     >>> bug_watch.addActivity()
     >>> logout()
@@ -299,7 +296,9 @@ watch" button on the watch's page.
     >>> from lp.testing.sampledata import ADMIN_EMAIL
     >>> login(ADMIN_EMAIL)
     >>> bug_watch = factory.makeBugWatch()
-    >>> removeSecurityProxy(bug_watch).lastchecked = datetime.now(utc)
+    >>> removeSecurityProxy(bug_watch).lastchecked = datetime.now(
+    ...     timezone.utc
+    ... )
     >>> watch_url = "http://bugs.launchpad.test/bugs/%s/+watch/%s"; % (
     ...     bug_watch.bug.id,
     ...     bug_watch.id,
diff --git a/lib/lp/bugs/subscribers/buglastupdated.py b/lib/lp/bugs/subscribers/buglastupdated.py
index a3dbfb4..8648166 100644
--- a/lib/lp/bugs/subscribers/buglastupdated.py
+++ b/lib/lp/bugs/subscribers/buglastupdated.py
@@ -3,9 +3,8 @@
 
 """Subscriber functions to update IBug.date_last_updated."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from lazr.lifecycle.interfaces import IObjectModifiedEvent
 from zope.security.proxy import removeSecurityProxy
 
@@ -28,4 +27,4 @@ def update_bug_date_last_updated(object, event):
             "Event handler expects object implementing IBug or IHasBug. "
             "Got: %s" % repr(object)
         )
-    removeSecurityProxy(bug).date_last_updated = datetime.now(pytz.UTC)
+    removeSecurityProxy(bug).date_last_updated = datetime.now(timezone.utc)
diff --git a/lib/lp/bugs/tests/bug.py b/lib/lp/bugs/tests/bug.py
index 3cc9b4b..e8e07cf 100644
--- a/lib/lp/bugs/tests/bug.py
+++ b/lib/lp/bugs/tests/bug.py
@@ -6,10 +6,9 @@
 import json
 import re
 import textwrap
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-from pytz import UTC
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -207,7 +206,7 @@ def create_old_bug(
             bugtracker=external_bugtracker,
             remotebug="1234",
         )
-    date = datetime.now(UTC) - timedelta(days=days_old)
+    date = datetime.now(timezone.utc) - timedelta(days=days_old)
     removeSecurityProxy(bug).date_last_updated = date
     return bugtask
 
@@ -232,7 +231,9 @@ def summarize_bugtasks(bugtasks):
             % (
                 title,
                 bugtask in expirable_bugtasks,
-                (datetime.now(UTC) - bugtask.bug.date_last_updated).days,
+                (
+                    datetime.now(timezone.utc) - bugtask.bug.date_last_updated
+                ).days,
                 bugtask.status.title,
                 bugtask.assignee is not None,
                 bugtask.bug.duplicateof is not None,
diff --git a/lib/lp/bugs/tests/bugs-emailinterface.rst b/lib/lp/bugs/tests/bugs-emailinterface.rst
index 61b1ef7..b0d3575 100644
--- a/lib/lp/bugs/tests/bugs-emailinterface.rst
+++ b/lib/lp/bugs/tests/bugs-emailinterface.rst
@@ -3192,11 +3192,9 @@ we'll create a new bug on firefox and link it to a remote bug.
     >>> firefox = getUtility(IProductSet).getByName("firefox")
     >>> no_priv = getUtility(IPersonSet).getByName("no-priv")
 
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> import pytz
-    >>> creation_date = datetime(
-    ...     2008, 4, 12, 10, 12, 12, tzinfo=pytz.timezone("UTC")
-    ... )
+    >>> creation_date = datetime(2008, 4, 12, 10, 12, 12, tzinfo=timezone.utc)
 
 We create the initial bug message separately from the bug itself so that
 we can ensure that its datecreated field is set correctly. This is
diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py
index 870686c..7fa1c76 100644
--- a/lib/lp/bugs/tests/test_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugnomination.py
@@ -3,6 +3,8 @@
 
 """Tests related to bug nominations."""
 
+from datetime import timezone
+
 from zope.component import getUtility
 
 from lp.app.errors import NotFoundError
@@ -42,7 +44,7 @@ class BugNominationTestCase(TestCaseWithFactory):
         self.assertIsNone(nomination.productseries)
         self.assertEqual(BugNominationStatus.PROPOSED, nomination.status)
         self.assertIsNone(nomination.date_decided)
-        self.assertEqual("UTC", nomination.date_created.tzname())
+        self.assertIs(timezone.utc, nomination.date_created.tzinfo)
 
     def test_target_distroseries(self):
         # The target property returns the distroseries if it is not None.
diff --git a/lib/lp/bugs/tests/test_bugnotification.py b/lib/lp/bugs/tests/test_bugnotification.py
index 47f764e..ec70cd9 100644
--- a/lib/lp/bugs/tests/test_bugnotification.py
+++ b/lib/lp/bugs/tests/test_bugnotification.py
@@ -3,9 +3,8 @@
 
 """Tests related to bug notifications."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from storm.store import Store
 from zope.component import getUtility
@@ -578,7 +577,7 @@ class TestGetDeferredNotifications(TestCaseWithFactory):
             "subject",
             "a comment.",
             bug.owner,
-            datecreated=datetime.now(pytz.UTC),
+            datecreated=datetime.now(timezone.utc),
         )
         self.bns.addNotification(
             bug, False, message, empty_recipients, None, deferred=True
diff --git a/lib/lp/bugs/tests/test_bugs_webservice.py b/lib/lp/bugs/tests/test_bugs_webservice.py
index fcd29c0..29be4de 100644
--- a/lib/lp/bugs/tests/test_bugs_webservice.py
+++ b/lib/lp/bugs/tests/test_bugs_webservice.py
@@ -6,9 +6,8 @@
 import io
 import json
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import six
 from lazr.lifecycle.interfaces import IDoNotSnapshot
 from storm.store import Store
@@ -487,7 +486,7 @@ class TestBugDateLastUpdated(TestCaseWithFactory):
 
     def make_old_bug(self):
         bug = self.factory.makeBug()
-        one_year_ago = datetime.now(pytz.UTC) - timedelta(days=365)
+        one_year_ago = datetime.now(timezone.utc) - timedelta(days=365)
         removeSecurityProxy(bug).date_last_updated = one_year_ago
         owner = bug.owner
         with person_logged_in(owner):
diff --git a/lib/lp/bugs/tests/test_bugtracker.py b/lib/lp/bugs/tests/test_bugtracker.py
index e54512b..5c9cdca 100644
--- a/lib/lp/bugs/tests/test_bugtracker.py
+++ b/lib/lp/bugs/tests/test_bugtracker.py
@@ -2,14 +2,13 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import unittest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from doctest import ELLIPSIS, NORMALIZE_WHITESPACE, DocTestSuite
 from urllib.parse import urlencode
 
 import responses
 import transaction
 from lazr.lifecycle.snapshot import Snapshot
-from pytz import utc
 from testtools.matchers import Equals, MatchesListwise, MatchesStructure
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
@@ -92,7 +91,7 @@ class BugTrackerTestCase(TestCaseWithFactory):
         for i in range(5):
             self.factory.makeBugWatch(bugtracker=self.bug_tracker)
 
-        self.now = datetime.now(utc)
+        self.now = datetime.now(timezone.utc)
 
     def test_multi_product_constraints_observed(self):
         """BugTrackers for which multi_product=True should return None
@@ -168,7 +167,7 @@ class BugTrackerTestCase(TestCaseWithFactory):
         self.assertTrue(bug_tracker.watches_ready_to_check.is_empty())
         # If we set its next_check date, it will be ready.
         removeSecurityProxy(bug_watch).next_check = datetime.now(
-            utc
+            timezone.utc
         ) - timedelta(hours=1)
         self.assertTrue(1, bug_tracker.watches_ready_to_check.count())
         self.assertEqual(bug_watch, bug_tracker.watches_ready_to_check.one())
diff --git a/lib/lp/bugs/tests/test_bugwatch.py b/lib/lp/bugs/tests/test_bugwatch.py
index a71a7d0..8cf900d 100644
--- a/lib/lp/bugs/tests/test_bugwatch.py
+++ b/lib/lp/bugs/tests/test_bugwatch.py
@@ -4,13 +4,12 @@
 """Tests for BugWatchSet."""
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import List, Optional
 from urllib.parse import urlunsplit
 
 import transaction
 from lazr.lifecycle.snapshot import Snapshot
-from pytz import utc
 from storm.store import Store
 from testscenarios import WithScenarios, load_tests_apply_scenarios
 from zope.component import getUtility
@@ -734,9 +733,9 @@ class TestBugWatchResetting(TestCaseWithFactory):
         self.bug_watch = self.factory.makeBugWatch()
         naked = removeSecurityProxy(self.bug_watch)
         naked.last_error_type = BugWatchActivityStatus.BUG_NOT_FOUND
-        naked.lastchanged = datetime.now(utc) - timedelta(days=1)
-        naked.lastchecked = datetime.now(utc) - timedelta(days=1)
-        naked.next_check = datetime.now(utc) + timedelta(days=7)
+        naked.lastchanged = datetime.now(timezone.utc) - timedelta(days=1)
+        naked.lastchecked = datetime.now(timezone.utc) - timedelta(days=1)
+        naked.next_check = datetime.now(timezone.utc) + timedelta(days=7)
         naked.remote_importance = "IMPORTANT"
         naked.remotestatus = "FIXED"
         self.default_bug_watch_fields = [
diff --git a/lib/lp/bugs/tests/test_cve.py b/lib/lp/bugs/tests/test_cve.py
index 8a7b50d..c3e7c4f 100644
--- a/lib/lp/bugs/tests/test_cve.py
+++ b/lib/lp/bugs/tests/test_cve.py
@@ -3,9 +3,8 @@
 
 """CVE related tests."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from testtools.matchers import MatchesStructure
 from testtools.testcase import ExpectedException
 from zope.component import getUtility
@@ -171,7 +170,7 @@ class TestCve(TestCaseWithFactory):
         )
 
     def test_cveset_new_method_parameters(self):
-        today = datetime.now(tz=pytz.UTC)
+        today = datetime.now(tz=timezone.utc)
         cvss = {"nvd": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}
         cve = getUtility(ICveSet).new(
             sequence="2099-1234",
diff --git a/lib/lp/bugs/tests/test_vulnerability.py b/lib/lp/bugs/tests/test_vulnerability.py
index 9a88d68..3289b07 100644
--- a/lib/lp/bugs/tests/test_vulnerability.py
+++ b/lib/lp/bugs/tests/test_vulnerability.py
@@ -3,10 +3,9 @@
 
 """Tests for lp.bugs.interface.IVulnerability exposed via the web service."""
 
-import datetime
 import json
+from datetime import datetime, timezone
 
-import pytz
 from testtools.matchers import MatchesStructure
 from zope.security.proxy import removeSecurityProxy
 
@@ -46,7 +45,7 @@ class TestVulnerabilityWebService(TestCaseWithFactory):
             permission=OAuthPermission.WRITE_PRIVATE,
             default_api_version="devel",
         )
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         response = webservice.patch(
             vulnerability_url,
@@ -214,7 +213,7 @@ class TestVulnerabilityWebService(TestCaseWithFactory):
             permission=OAuthPermission.WRITE_PRIVATE,
             default_api_version="devel",
         )
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         response = webservice.patch(
             vulnerability_url,
@@ -249,7 +248,7 @@ class TestVulnerabilityWebService(TestCaseWithFactory):
     def test_user_without_edit_permissions_cannot_edit_vulnerability(self):
         vulnerability = removeSecurityProxy(self.factory.makeVulnerability())
         random_user = self.factory.makePerson()
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         vulnerability_url = api_url(vulnerability)
 
         webservice = webservice_for_person(
diff --git a/lib/lp/buildmaster/browser/builder.py b/lib/lp/buildmaster/browser/builder.py
index 2d3343a..a45ab35 100644
--- a/lib/lp/buildmaster/browser/builder.py
+++ b/lib/lp/buildmaster/browser/builder.py
@@ -15,10 +15,9 @@ __all__ = [
 ]
 
 import operator
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from itertools import groupby
 
-import pytz
 from lazr.restful.utils import smartquote
 from zope.component import getUtility
 from zope.event import notify
@@ -166,7 +165,7 @@ class CleanInfoMixin:
 
     @cachedproperty
     def _now(self):
-        return datetime.now(pytz.UTC)
+        return datetime.now(timezone.utc)
 
     def getCleanInfo(self, builder):
         duration = self._now - builder.date_clean_status_changed
diff --git a/lib/lp/buildmaster/doc/buildqueue.rst b/lib/lp/buildmaster/doc/buildqueue.rst
index 49679ee..d0e192b 100644
--- a/lib/lp/buildmaster/doc/buildqueue.rst
+++ b/lib/lp/buildmaster/doc/buildqueue.rst
@@ -48,7 +48,8 @@ The timestamp of when the job was dispatched is provided as datetime
 instances:
 
     >>> bq.date_started
-    datetime.datetime(2005, 6, 15, 9, 20, 12, 820778, tzinfo=<UTC>)
+    datetime.datetime(2005, 6, 15, 9, 20, 12, 820778,
+        tzinfo=datetime.timezone.utc)
 
 Check Builder foreign key, which indicated which builder 'is processing'
 the job in question:
diff --git a/lib/lp/buildmaster/model/builder.py b/lib/lp/buildmaster/model/builder.py
index ea1a4ff..9de4908 100644
--- a/lib/lp/buildmaster/model/builder.py
+++ b/lib/lp/buildmaster/model/builder.py
@@ -7,7 +7,8 @@ __all__ = [
     "BuilderSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.expr import Coalesce, Count, Sum
 from storm.properties import Bool, DateTime, Int, Unicode
 from storm.references import Reference
@@ -72,7 +73,7 @@ class Builder(StormBase):
         enum=BuilderCleanStatus, default=BuilderCleanStatus.DIRTY
     )
     vm_reset_protocol = DBEnum(enum=BuilderResetProtocol)
-    date_clean_status_changed = DateTime(tzinfo=pytz.UTC)
+    date_clean_status_changed = DateTime(tzinfo=timezone.utc)
 
     def __init__(
         self,
diff --git a/lib/lp/buildmaster/model/buildfarmjob.py b/lib/lp/buildmaster/model/buildfarmjob.py
index 14303c4..5549891 100644
--- a/lib/lp/buildmaster/model/buildfarmjob.py
+++ b/lib/lp/buildmaster/model/buildfarmjob.py
@@ -8,9 +8,8 @@ __all__ = [
     "SpecificBuildFarmJobSourceMixin",
 ]
 
-import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.expr import Desc, LeftJoin, Or
 from storm.locals import DateTime, Int, Reference
 from storm.store import Store
@@ -62,11 +61,11 @@ class BuildFarmJob(StormBase):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", allow_none=False, tzinfo=pytz.UTC
+        name="date_created", allow_none=False, tzinfo=timezone.utc
     )
 
     date_finished = DateTime(
-        name="date_finished", allow_none=True, tzinfo=pytz.UTC
+        name="date_finished", allow_none=True, tzinfo=timezone.utc
     )
 
     builder_id = Int(name="builder", allow_none=True)
@@ -238,7 +237,7 @@ class BuildFarmJobMixin:
         # If we're starting to build, set date_started and
         # date_first_dispatched if required.
         if self.date_started is None and status == BuildStatus.BUILDING:
-            self.date_started = date_started or datetime.datetime.now(pytz.UTC)
+            self.date_started = date_started or datetime.now(timezone.utc)
             if self.date_first_dispatched is None:
                 self.date_first_dispatched = self.date_started
 
@@ -259,7 +258,7 @@ class BuildFarmJobMixin:
             # the duration spent building locally.
             self.build_farm_job.date_finished = (
                 self.date_finished
-            ) = date_finished or datetime.datetime.now(pytz.UTC)
+            ) = date_finished or datetime.now(timezone.utc)
             self.emitMetric("finished", status=status.name)
 
     def gotFailure(self):
diff --git a/lib/lp/buildmaster/model/buildqueue.py b/lib/lp/buildmaster/model/buildqueue.py
index 1bbcd03..bedc319 100644
--- a/lib/lp/buildmaster/model/buildqueue.py
+++ b/lib/lp/buildmaster/model/buildqueue.py
@@ -8,11 +8,10 @@ __all__ = [
 
 import json
 import logging
-from datetime import datetime
+from datetime import datetime, timezone
 from itertools import groupby
 from operator import attrgetter
 
-import pytz
 from storm.expr import SQL, Cast, Coalesce, Desc, Exists, Or
 from storm.properties import Bool, DateTime, Int, TimeDelta, Unicode
 from storm.references import Reference
@@ -89,7 +88,7 @@ class BuildQueue(StormBase):
     _build_farm_job_id = Int(name="build_farm_job")
     _build_farm_job = Reference(_build_farm_job_id, "BuildFarmJob.id")
     status = DBEnum(enum=BuildQueueStatus, default=BuildQueueStatus.WAITING)
-    date_started = DateTime(tzinfo=pytz.UTC)
+    date_started = DateTime(tzinfo=timezone.utc)
 
     builder_id = Int(name="builder", default=None)
     builder = Reference(builder_id, "Builder.id")
@@ -237,7 +236,7 @@ class BuildQueue(StormBase):
     @staticmethod
     def _now():
         """Return current time (UTC).  Overridable for test purposes."""
-        return datetime.now(pytz.utc)
+        return datetime.now(timezone.utc)
 
 
 @implementer(IBuildQueueSet)
diff --git a/lib/lp/buildmaster/queuedepth.py b/lib/lp/buildmaster/queuedepth.py
index 52e561b..702af11 100644
--- a/lib/lp/buildmaster/queuedepth.py
+++ b/lib/lp/buildmaster/queuedepth.py
@@ -6,9 +6,8 @@ __all__ = [
 ]
 
 from collections import defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from storm.expr import Count
 
 from lp.buildmaster.enums import BuildQueueStatus
@@ -132,7 +131,7 @@ def estimate_time_to_next_builder(bq, now=None):
 
     head_job_processor, head_job_virtualized = head_job_platform
 
-    now = now or datetime.now(utc)
+    now = now or datetime.now(timezone.utc)
     delay_query = """
         SELECT MIN(
             CASE WHEN
@@ -345,5 +344,7 @@ def estimate_job_start_time(bq, now=None):
 
     # A job will not get dispatched in less than 5 seconds no matter what.
     start_time = max(5, min_wait_time + sum_of_delays)
-    result = (now or datetime.now(utc)) + timedelta(seconds=start_time)
+    result = (now or datetime.now(timezone.utc)) + timedelta(
+        seconds=start_time
+    )
     return result
diff --git a/lib/lp/buildmaster/stories/builder-views.rst b/lib/lp/buildmaster/stories/builder-views.rst
index 3c3f280..ad45813 100644
--- a/lib/lp/buildmaster/stories/builder-views.rst
+++ b/lib/lp/buildmaster/stories/builder-views.rst
@@ -222,12 +222,11 @@ all the 'private' information is exposed.
     >>> print(admin_view.context.failnotes)
     None
 
-    >>> import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.security.proxy import removeSecurityProxy
-    >>> removeSecurityProxy(private_job).date_started = datetime.datetime.now(
-    ...     pytz.UTC
-    ... ) - datetime.timedelta(10)
+    >>> removeSecurityProxy(private_job).date_started = datetime.now(
+    ...     timezone.utc
+    ... ) - timedelta(10)
     >>> print(admin_view.current_build_duration)
     10 days...
 
@@ -393,12 +392,11 @@ The 'virtual' builder category is also available in BuilderSetView as a
 We change the sampledata to create a pending build in for the 386
 processor queue in the PPA category.
 
-    >>> import datetime
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> any_failed_build = cprov.archive.getBuildRecords(
     ...     build_state=BuildStatus.FAILEDTOBUILD
     ... )[0]
-    >>> one_minute = datetime.timedelta(seconds=60)
+    >>> one_minute = timedelta(seconds=60)
     >>> any_failed_build.retry()
     >>> removeSecurityProxy(
     ...     any_failed_build.buildqueue_record
diff --git a/lib/lp/buildmaster/tests/test_buildfarmjob.py b/lib/lp/buildmaster/tests/test_buildfarmjob.py
index 94de00b..e0973fe 100644
--- a/lib/lp/buildmaster/tests/test_buildfarmjob.py
+++ b/lib/lp/buildmaster/tests/test_buildfarmjob.py
@@ -3,10 +3,9 @@
 
 """Tests for `IBuildFarmJob`."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import Type
 
-import pytz
 from storm.store import Store
 from testtools.matchers import GreaterThan
 from zope.component import getUtility
@@ -94,7 +93,7 @@ class TestBuildFarmJob(TestBuildFarmJobBase, TestCaseWithFactory):
         # date_created can be passed optionally when creating a
         # build farm job to ensure we don't get identical timestamps
         # when transactions are committed.
-        ten_years_ago = datetime.now(pytz.UTC) - timedelta(365 * 10)
+        ten_years_ago = datetime.now(timezone.utc) - timedelta(365 * 10)
         build_farm_job = getUtility(IBuildFarmJobSource).new(
             job_type=BuildFarmJobType.PACKAGEBUILD, date_created=ten_years_ago
         )
@@ -129,7 +128,7 @@ class TestBuildFarmJobMixin(TestCaseWithFactory):
     def test_duration_set(self):
         # If both start and finished are defined, the duration will be
         # returned.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         duration = timedelta(1)
         self.build_farm_job.updateStatus(
             BuildStatus.BUILDING, date_started=now
@@ -323,15 +322,15 @@ class TestBuildFarmJobSet(TestBuildFarmJobBase, TestCaseWithFactory):
         # Results are returned with the oldest build last.
         build_1 = self.makeBuildFarmJob(
             builder=self.builder,
-            date_finished=datetime(2008, 10, 10, tzinfo=pytz.UTC),
+            date_finished=datetime(2008, 10, 10, tzinfo=timezone.utc),
         )
         build_2 = self.makeBuildFarmJob(
             builder=self.builder,
-            date_finished=datetime(2008, 11, 10, tzinfo=pytz.UTC),
+            date_finished=datetime(2008, 11, 10, tzinfo=timezone.utc),
         )
         build_3 = self.makeBuildFarmJob(
             builder=self.builder,
-            date_finished=datetime(2008, 9, 10, tzinfo=pytz.UTC),
+            date_finished=datetime(2008, 9, 10, tzinfo=timezone.utc),
         )
 
         result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
diff --git a/lib/lp/buildmaster/tests/test_queuedepth.py b/lib/lp/buildmaster/tests/test_queuedepth.py
index 5c215cc..d6ac8a4 100644
--- a/lib/lp/buildmaster/tests/test_queuedepth.py
+++ b/lib/lp/buildmaster/tests/test_queuedepth.py
@@ -2,9 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 """Test BuildQueue start time estimation."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -29,7 +28,7 @@ from lp.testing.layers import LaunchpadZopelessLayer
 
 def check_mintime_to_builder(test, bq, min_time):
     """Test the estimated time until a builder becomes available."""
-    time_stamp = bq.date_started or datetime.now(utc)
+    time_stamp = bq.date_started or datetime.now(timezone.utc)
     delay = estimate_time_to_next_builder(
         removeSecurityProxy(bq), now=time_stamp
     )
@@ -43,9 +42,9 @@ def check_mintime_to_builder(test, bq, min_time):
 def set_remaining_time_for_running_job(bq, remainder):
     """Set remaining running time for job."""
     offset = bq.estimated_duration.seconds - remainder
-    removeSecurityProxy(bq).date_started = datetime.now(utc) - timedelta(
-        seconds=offset
-    )
+    removeSecurityProxy(bq).date_started = datetime.now(
+        timezone.utc
+    ) - timedelta(seconds=offset)
 
 
 def check_delay_for_job(test, the_job, delay):
@@ -70,7 +69,7 @@ def builders_for_job(job):
 
 
 def check_estimate(test, job, delay_in_seconds):
-    time_stamp = job.date_started or datetime.now(utc)
+    time_stamp = job.date_started or datetime.now(timezone.utc)
     estimate = job.getEstimatedJobStartTime(now=time_stamp)
     if delay_in_seconds is None:
         test.assertEqual(
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index 836fb56..6153622 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -6,10 +6,9 @@
 import base64
 import json
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.parse import parse_qs, urlsplit
 
-import pytz
 import responses
 import soupmatchers
 import transaction
@@ -589,7 +588,7 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
             member_of=[getUtility(ILaunchpadCelebrities).ppa_admin]
         )
         login_person(self.person)
-        date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+        date_created = datetime(2000, 1, 1, tzinfo=timezone.utc)
         recipe = self.factory.makeCharmRecipe(
             registrant=self.person, date_created=date_created
         )
@@ -894,7 +893,7 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
 
     def test_edit_recipe_sets_date_last_modified(self):
         # Editing a charm recipe sets the date_last_modified property.
-        date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+        date_created = datetime(2000, 1, 1, tzinfo=timezone.utc)
         recipe = self.factory.makeCharmRecipe(
             registrant=self.person, date_created=date_created
         )
@@ -1353,7 +1352,7 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         if recipe is None:
             recipe = self.makeCharmRecipe()
         if date_created is None:
-            date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
+            date_created = datetime.now(timezone.utc) - timedelta(hours=1)
         build = self.factory.makeCharmRecipeBuild(
             requester=self.person,
             recipe=recipe,
@@ -1494,7 +1493,7 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
             request = recipe.requestBuilds(recipe.owner)
         job = removeSecurityProxy(removeSecurityProxy(request)._job)
         job.job._status = JobStatus.FAILED
-        job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1)
+        job.job.date_finished = datetime.now(timezone.utc) - timedelta(hours=1)
         job.error_message = "Boom"
         self.assertTextMatchesExpressionIgnoreWhitespace(
             r"""\
@@ -1519,7 +1518,7 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         recipe = self.makeCharmRecipe()
         # Create oldest builds first so that they sort properly by id.
         date_gen = time_counter(
-            datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+            datetime(2000, 1, 1, tzinfo=timezone.utc), timedelta(days=1)
         )
         builds = [
             self.makeBuild(recipe=recipe, date_created=next(date_gen))
diff --git a/lib/lp/charms/browser/tests/test_charmrecipelisting.py b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
index cf94126..50e8b0a 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipelisting.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
@@ -3,10 +3,9 @@
 
 """Test charm recipe listings."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from functools import partial
 
-import pytz
 import soupmatchers
 from testtools.matchers import MatchesAll, Not
 from zope.security.proxy import removeSecurityProxy
@@ -264,7 +263,7 @@ class TestCharmRecipeListing(BrowserTestCase):
         repository = self.factory.makeGitRepository()
         [ref] = self.factory.makeGitRefs(repository=repository)
         create_recipe = partial(self.factory.makeCharmRecipe, git_ref=ref)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(repository, link_matchers, False, 0, 3)
         link_matchers.extend(
@@ -277,7 +276,7 @@ class TestCharmRecipeListing(BrowserTestCase):
     def test_git_ref_batches_recipes(self):
         [ref] = self.factory.makeGitRefs()
         create_recipe = partial(self.factory.makeCharmRecipe, git_ref=ref)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(ref, link_matchers, False, 0, 3)
         link_matchers.extend(
@@ -292,7 +291,7 @@ class TestCharmRecipeListing(BrowserTestCase):
         create_recipe = partial(
             self.factory.makeCharmRecipe, registrant=owner, owner=owner
         )
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(owner, link_matchers, False, 0, 3)
         link_matchers.extend(
@@ -306,7 +305,7 @@ class TestCharmRecipeListing(BrowserTestCase):
         project = self.factory.makeProduct()
         [ref] = self.factory.makeGitRefs(target=project)
         create_recipe = partial(self.factory.makeCharmRecipe, git_ref=ref)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(project, link_matchers, False, 0, 3)
         link_matchers.extend(
diff --git a/lib/lp/charms/model/charmbase.py b/lib/lp/charms/model/charmbase.py
index bfff346..40b6f2f 100644
--- a/lib/lp/charms/model/charmbase.py
+++ b/lib/lp/charms/model/charmbase.py
@@ -7,7 +7,8 @@ __all__ = [
     "CharmBase",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.databases.postgres import JSON
 from storm.locals import DateTime, Int, Reference, Store
 from zope.interface import implementer
@@ -33,7 +34,7 @@ class CharmBase(StormBase):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
 
     registrant_id = Int(name="registrant", allow_none=False)
diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
index 566b61c..eb3891c 100644
--- a/lib/lp/charms/model/charmrecipe.py
+++ b/lib/lp/charms/model/charmrecipe.py
@@ -9,10 +9,9 @@ __all__ = [
 ]
 
 import base64
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter, itemgetter
 
-import pytz
 import yaml
 from lazr.lifecycle.event import ObjectCreatedEvent
 from pymacaroons import Macaroon
@@ -220,10 +219,10 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     registrant_id = Int(name="registrant", allow_none=False)
@@ -1187,7 +1186,7 @@ class CharmRecipeSet:
     @staticmethod
     def _findStaleRecipes():
         """Find recipes that need to be rebuilt."""
-        threshold_date = datetime.now(pytz.UTC) - timedelta(
+        threshold_date = datetime.now(timezone.utc) - timedelta(
             minutes=config.charms.auto_build_frequency
         )
         stale_clauses = [
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index d0656ce..b5e4fe6 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -8,10 +8,9 @@ __all__ = [
     "CharmRecipeBuild",
 ]
 
-from datetime import timedelta
+from datetime import timedelta, timezone
 from operator import attrgetter
 
-import pytz
 import six
 from storm.databases.postgres import JSON
 from storm.expr import Column, Table, With
@@ -106,16 +105,16 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
     virtualized = Bool(name="virtualized", allow_none=False)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_started = DateTime(
-        name="date_started", tzinfo=pytz.UTC, allow_none=True
+        name="date_started", tzinfo=timezone.utc, allow_none=True
     )
     date_finished = DateTime(
-        name="date_finished", tzinfo=pytz.UTC, allow_none=True
+        name="date_finished", tzinfo=timezone.utc, allow_none=True
     )
     date_first_dispatched = DateTime(
-        name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True
+        name="date_first_dispatched", tzinfo=timezone.utc, allow_none=True
     )
 
     builder_id = Int(name="builder", allow_none=True)
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index 0855d7e..0f3502b 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -5,11 +5,10 @@
 
 import base64
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from textwrap import dedent
 
 import iso8601
-import pytz
 import responses
 import transaction
 from fixtures import FakeLogger
@@ -1603,7 +1602,7 @@ class TestCharmRecipeSet(TestCaseWithFactory):
             )
             removeSecurityProxy(
                 removeSecurityProxy(build_request)._job
-            ).job.date_created = datetime.now(pytz.UTC) - timedelta(days=2)
+            ).job.date_created = datetime.now(timezone.utc) - timedelta(days=2)
         self.assertContentEqual([recipe], CharmRecipeSet._findStaleRecipes())
 
     def test_makeAutoBuilds(self):
@@ -1761,7 +1760,7 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         # If a previous build request is not recent and the recipe is stale,
         # ICharmRecipeSet.makeAutoBuilds requests builds.
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
-        one_day_ago = datetime.now(pytz.UTC) - timedelta(days=1)
+        one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
         build_request = self.factory.makeCharmRecipeBuildRequest(
             recipe=recipe, requester=recipe.owner
         )
@@ -1785,7 +1784,7 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         # request builds.
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         for timediff in timedelta(days=1), timedelta(minutes=30):
-            date_created = datetime.now(pytz.UTC) - timediff
+            date_created = datetime.now(timezone.utc) - timediff
             build_request = self.factory.makeCharmRecipeBuildRequest(
                 recipe=recipe, requester=recipe.owner
             )
diff --git a/lib/lp/charms/tests/test_charmrecipebuild.py b/lib/lp/charms/tests/test_charmrecipebuild.py
index ee7c786..cde6235 100644
--- a/lib/lp/charms/tests/test_charmrecipebuild.py
+++ b/lib/lp/charms/tests/test_charmrecipebuild.py
@@ -4,10 +4,9 @@
 """Test charm package build features."""
 
 import base64
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.request import urlopen
 
-import pytz
 from fixtures import FakeLogger
 from nacl.public import PrivateKey
 from pymacaroons import Macaroon
@@ -197,7 +196,7 @@ class TestCharmRecipeBuild(TestCaseWithFactory):
     def test_retry_resets_state(self):
         # Retrying a build resets most of the state attributes, but does
         # not modify the first dispatch time.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         build = self.factory.makeCharmRecipeBuild()
         build.updateStatus(BuildStatus.BUILDING, date_started=now)
         build.updateStatus(BuildStatus.FAILEDTOBUILD)
@@ -896,7 +895,7 @@ class TestCharmRecipeBuildWebservice(TestCaseWithFactory):
         # The basic properties of a charm recipe build are sensible.
         db_build = self.factory.makeCharmRecipeBuild(
             requester=self.person,
-            date_created=datetime(2021, 9, 15, 16, 21, 0, tzinfo=pytz.UTC),
+            date_created=datetime(2021, 9, 15, 16, 21, 0, tzinfo=timezone.utc),
         )
         build_url = api_url(db_build)
         logout()
diff --git a/lib/lp/code/browser/branch.py b/lib/lp/code/browser/branch.py
index 544176b..ae6115a 100644
--- a/lib/lp/code/browser/branch.py
+++ b/lib/lp/code/browser/branch.py
@@ -23,9 +23,8 @@ __all__ = [
 ]
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restful.fields import Reference
@@ -921,7 +920,7 @@ class BranchMirrorStatusView(LaunchpadFormView):
         """Is it likely that the branch is being mirrored in the next run of
         the puller?
         """
-        return self.context.next_mirror_time < datetime.now(pytz.UTC)
+        return self.context.next_mirror_time < datetime.now(timezone.utc)
 
     @property
     def mirror_disabled(self):
diff --git a/lib/lp/code/browser/tests/test_branch.py b/lib/lp/code/browser/tests/test_branch.py
index c42cbe0..e94c52b 100644
--- a/lib/lp/code/browser/tests/test_branch.py
+++ b/lib/lp/code/browser/tests/test_branch.py
@@ -3,10 +3,9 @@
 
 """Unit tests for BranchView."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 from textwrap import dedent
 
-import pytz
 from fixtures import FakeLogger
 from storm.store import Store
 from testtools.matchers import Equals
@@ -1051,7 +1050,7 @@ class TestBranchReviewerEditView(TestCaseWithFactory):
         # If the user has set the reviewer to be same and clicked on save,
         # then the underlying object hasn't really been changed, so the last
         # modified is not updated.
-        modified_date = datetime(2007, 1, 1, tzinfo=pytz.UTC)
+        modified_date = datetime(2007, 1, 1, tzinfo=timezone.utc)
         branch = self.factory.makeAnyBranch(date_created=modified_date)
         view = create_initialized_view(branch, "+reviewer")
         view.change_action.success({"reviewer": branch.owner})
diff --git a/lib/lp/code/browser/tests/test_branchmergeproposal.py b/lib/lp/code/browser/tests/test_branchmergeproposal.py
index 5042014..a010eec 100644
--- a/lib/lp/code/browser/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/browser/tests/test_branchmergeproposal.py
@@ -7,10 +7,9 @@ import doctest
 import hashlib
 import json
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from difflib import diff_bytes, unified_diff
 
-import pytz
 import transaction
 from fixtures import FakeLogger
 from lazr.lifecycle.event import ObjectModifiedEvent
@@ -1522,7 +1521,7 @@ class TestResubmitBrowserGit(GitHostingClientMixin, BrowserTestCase):
         author = self.factory.makePerson()
         with person_logged_in(author):
             author_email = author.preferredemail.email
-        author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
+        author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
         sha1s = [
             hashlib.sha1(("commit %d" % i).encode()).hexdigest()
             for i in range(count)
@@ -1723,9 +1722,9 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
         self.assertEqual([bug.default_bugtask], view.linked_bugtasks)
 
     def makeRevisionGroups(self):
-        review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
+        review_date = datetime(2009, 9, 10, tzinfo=timezone.utc)
         bmp = self.factory.makeBranchMergeProposal(date_created=review_date)
-        first_commit = datetime(2009, 9, 9, tzinfo=pytz.UTC)
+        first_commit = datetime(2009, 9, 9, tzinfo=timezone.utc)
         add_revision_to_branch(self.factory, bmp.source_branch, first_commit)
         login_person(bmp.registrant)
         bmp.requestReview(review_date)
@@ -1760,7 +1759,7 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
 
     def test_CodeReviewNewRevisions_implements_interface_bzr(self):
         # The browser helper class implements its interface.
-        review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
+        review_date = datetime(2009, 9, 10, tzinfo=timezone.utc)
         revision_date = review_date + timedelta(days=1)
         bmp = self.factory.makeBranchMergeProposal(date_created=review_date)
         add_revision_to_branch(self.factory, bmp.source_branch, revision_date)
@@ -1772,7 +1771,7 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
 
     def test_CodeReviewNewRevisions_implements_interface_git(self):
         # The browser helper class implements its interface.
-        review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
+        review_date = datetime(2009, 9, 10, tzinfo=timezone.utc)
         author = self.factory.makePerson()
         with person_logged_in(author):
             author_email = author.preferredemail.email
@@ -1866,7 +1865,7 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
         bmp = self.factory.makeBranchMergeProposalForGit()
         owner = bmp.source_git_repository.owner
         sha1 = hashlib.sha1(b"0").hexdigest()
-        commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
+        commit_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
         report1 = self.factory.makeRevisionStatusReport(
             user=owner,
             git_repository=bmp.source_git_repository,
@@ -1948,7 +1947,7 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
         # SHA-1 and can ask the repository for its unmerged commits.
         bmp = self.factory.makeBranchMergeProposalForGit()
         sha1 = hashlib.sha1(b"0").hexdigest()
-        commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
+        commit_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
         self.useFixture(
             GitHostingFixture(
                 log=[
@@ -2732,7 +2731,7 @@ class TestBranchMergeCandidateView(TestCaseWithFactory):
         bmp.approveBranch(
             owner,
             "some-rev",
-            datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC),
+            datetime(year=2008, month=9, day=10, tzinfo=timezone.utc),
         )
         view = create_initialized_view(bmp, "+link-summary")
         self.assertEqual("Eric on 2008-09-10", view.status_title)
@@ -2747,7 +2746,7 @@ class TestBranchMergeCandidateView(TestCaseWithFactory):
         bmp.rejectBranch(
             owner,
             "some-rev",
-            datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC),
+            datetime(year=2008, month=9, day=10, tzinfo=timezone.utc),
         )
         view = create_initialized_view(bmp, "+link-summary")
         self.assertEqual("Eric on 2008-09-10", view.status_title)
@@ -2986,12 +2985,12 @@ class TestLatestProposalsForEachBranchMixin:
         # If each proposal targets a different branch, each will be returned.
         bmp1 = self._makeBranchMergeProposal(
             date_created=(
-                datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=9, day=10, tzinfo=timezone.utc)
             )
         )
         bmp2 = self._makeBranchMergeProposal(
             date_created=(
-                datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=10, day=10, tzinfo=timezone.utc)
             )
         )
         self.assertEqual(
@@ -3002,12 +3001,12 @@ class TestLatestProposalsForEachBranchMixin:
         # If the proposal is not visible to the user, they are not returned.
         bmp1 = self._makeBranchMergeProposal(
             date_created=(
-                datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=9, day=10, tzinfo=timezone.utc)
             )
         )
         bmp2 = self._makeBranchMergeProposal(
             date_created=(
-                datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=10, day=10, tzinfo=timezone.utc)
             )
         )
         self._setBranchInvisible(bmp2.merge_source)
@@ -3020,13 +3019,13 @@ class TestLatestProposalsForEachBranchMixin:
         # returned.
         bmp1 = self._makeBranchMergeProposal(
             date_created=(
-                datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=9, day=10, tzinfo=timezone.utc)
             )
         )
         bmp2 = self._makeBranchMergeProposal(
             merge_target=bmp1.merge_target,
             date_created=(
-                datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)
+                datetime(year=2008, month=10, day=10, tzinfo=timezone.utc)
             ),
         )
         self.assertEqual(
diff --git a/lib/lp/code/browser/tests/test_branchmergeproposallisting.py b/lib/lp/code/browser/tests/test_branchmergeproposallisting.py
index cdcd79e..9e50d59 100644
--- a/lib/lp/code/browser/tests/test_branchmergeproposallisting.py
+++ b/lib/lp/code/browser/tests/test_branchmergeproposallisting.py
@@ -3,9 +3,8 @@
 
 """Unit tests for BranchMergeProposal listing views."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from testtools.content import text_content
 from testtools.matchers import LessThan
@@ -911,10 +910,10 @@ class TestBranchMergeProposalListingItemMixin:
         # If the proposal is in needs review, the sort_key will be the
         # date_review_requested.
         bmp = self.factory.makeBranchMergeProposal(
-            date_created=datetime(2009, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2009, 6, 1, tzinfo=timezone.utc)
         )
         login_person(bmp.registrant)
-        request_date = datetime(2009, 7, 1, tzinfo=pytz.UTC)
+        request_date = datetime(2009, 7, 1, tzinfo=timezone.utc)
         bmp.requestReview(request_date)
         item = BranchMergeProposalListingItem(bmp, None, None)
         self.assertEqual(request_date, item.sort_key)
@@ -923,15 +922,15 @@ class TestBranchMergeProposalListingItemMixin:
         # If the proposal is approved, the sort_key will default to the
         # date_review_requested.
         bmp = self.factory.makeBranchMergeProposal(
-            date_created=datetime(2009, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2009, 6, 1, tzinfo=timezone.utc)
         )
         login_person(bmp.target_branch.owner)
-        request_date = datetime(2009, 7, 1, tzinfo=pytz.UTC)
+        request_date = datetime(2009, 7, 1, tzinfo=timezone.utc)
         bmp.requestReview(request_date)
         bmp.approveBranch(
             bmp.target_branch.owner,
             "rev-id",
-            datetime(2009, 8, 1, tzinfo=pytz.UTC),
+            datetime(2009, 8, 1, tzinfo=timezone.utc),
         )
         item = BranchMergeProposalListingItem(bmp, None, None)
         self.assertEqual(request_date, item.sort_key)
@@ -940,10 +939,10 @@ class TestBranchMergeProposalListingItemMixin:
         # If the proposal is approved and the review has been bypassed, the
         # date_reviewed is used.
         bmp = self.factory.makeBranchMergeProposal(
-            date_created=datetime(2009, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2009, 6, 1, tzinfo=timezone.utc)
         )
         login_person(bmp.target_branch.owner)
-        review_date = datetime(2009, 8, 1, tzinfo=pytz.UTC)
+        review_date = datetime(2009, 8, 1, tzinfo=timezone.utc)
         bmp.approveBranch(bmp.target_branch.owner, "rev-id", review_date)
         item = BranchMergeProposalListingItem(bmp, None, None)
         self.assertEqual(review_date, item.sort_key)
@@ -951,7 +950,7 @@ class TestBranchMergeProposalListingItemMixin:
     def test_sort_key_wip(self):
         # If the proposal is a work in progress, the date_created is used.
         bmp = self.factory.makeBranchMergeProposal(
-            date_created=datetime(2009, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2009, 6, 1, tzinfo=timezone.utc)
         )
         login_person(bmp.target_branch.owner)
         item = BranchMergeProposalListingItem(bmp, None, None)
@@ -980,13 +979,13 @@ class ActiveReviewSortingTestMixin:
         product = self.factory.makeProduct()
         bmp1 = self._makeBranchMergeProposal(target=product)
         login_person(bmp1.merge_source.owner)
-        bmp1.requestReview(datetime(2009, 6, 1, tzinfo=pytz.UTC))
+        bmp1.requestReview(datetime(2009, 6, 1, tzinfo=timezone.utc))
         bmp2 = self._makeBranchMergeProposal(target=product)
         login_person(bmp2.merge_source.owner)
-        bmp2.requestReview(datetime(2009, 3, 1, tzinfo=pytz.UTC))
+        bmp2.requestReview(datetime(2009, 3, 1, tzinfo=timezone.utc))
         bmp3 = self._makeBranchMergeProposal(target=product)
         login_person(bmp3.merge_source.owner)
-        bmp3.requestReview(datetime(2009, 1, 1, tzinfo=pytz.UTC))
+        bmp3.requestReview(datetime(2009, 1, 1, tzinfo=timezone.utc))
         login(ANONYMOUS)
         view = create_initialized_view(
             product, name="+activereviews", rootsite="code"
diff --git a/lib/lp/code/browser/tests/test_gitref.py b/lib/lp/code/browser/tests/test_gitref.py
index ffaa0d0..073af5e 100644
--- a/lib/lp/code/browser/tests/test_gitref.py
+++ b/lib/lp/code/browser/tests/test_gitref.py
@@ -5,10 +5,9 @@
 
 import hashlib
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 from textwrap import dedent
 
-import pytz
 import soupmatchers
 from fixtures import FakeLogger
 from storm.store import Store
@@ -951,7 +950,7 @@ class TestGitRefView(BrowserTestCase):
         with admin_logged_in():
             author_emails = [author.preferredemail.email for author in authors]
         dates = [
-            datetime(2015, 1, day + 1, tzinfo=pytz.UTC) for day in range(5)
+            datetime(2015, 1, day + 1, tzinfo=timezone.utc) for day in range(5)
         ]
         return [
             {
diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py
index 51bf64b..8ab604b 100644
--- a/lib/lp/code/browser/tests/test_gitrepository.py
+++ b/lib/lp/code/browser/tests/test_gitrepository.py
@@ -6,12 +6,11 @@
 import base64
 import doctest
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 from itertools import chain
 from operator import attrgetter
 from textwrap import dedent
 
-import pytz
 import soupmatchers
 import transaction
 from fixtures import FakeLogger
@@ -994,7 +993,7 @@ class TestGitRepositoryBranches(BrowserTestCase):
         # The number of queries is constant in the number of refs.
         person = self.factory.makePerson()
         repository = self.factory.makeGitRepository(owner=person)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         def create_ref():
             with person_logged_in(person):
@@ -1063,7 +1062,7 @@ class TestGitRepositoryEditReviewerView(TestCaseWithFactory):
         # If the user has set the reviewer to be same and clicked on save,
         # then the underlying object hasn't really been changed, so the last
         # modified is not updated.
-        modified_date = datetime(2007, 1, 1, tzinfo=pytz.UTC)
+        modified_date = datetime(2007, 1, 1, tzinfo=timezone.utc)
         repository = self.factory.makeGitRepository(date_created=modified_date)
         view = create_initialized_view(repository, "+reviewer")
         view.change_action.success({"reviewer": repository.owner})
diff --git a/lib/lp/code/browser/tests/test_product.py b/lib/lp/code/browser/tests/test_product.py
index 99148c8..013c0a7 100644
--- a/lib/lp/code/browser/tests/test_product.py
+++ b/lib/lp/code/browser/tests/test_product.py
@@ -3,9 +3,8 @@
 
 """Tests for the product view classes and templates."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.component import getUtility
 from zope.testbrowser.browser import LinkNotFoundError
 
@@ -274,7 +273,7 @@ class TestProductBranchSummaryView(ProductTestBase):
         self.factory.makePerson(email="cthulu@xxxxxxxxxxx")
         product, branch = self.makeProductAndDevelopmentFocusBranch()
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=30), timedelta(days=1)
+            datetime.now(timezone.utc) - timedelta(days=30), timedelta(days=1)
         )
         self.factory.makeRevisionsForBranch(
             branch, author="cthulu@xxxxxxxxxxx", date_generator=date_generator
@@ -294,7 +293,7 @@ class TestProductBranchSummaryView(ProductTestBase):
             owner=fsm, information_type=InformationType.USERDATA
         )
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=30), timedelta(days=1)
+            datetime.now(timezone.utc) - timedelta(days=30), timedelta(days=1)
         )
         login_person(fsm)
         self.factory.makeRevisionsForBranch(
@@ -326,7 +325,7 @@ class TestProductBranchSummaryView(ProductTestBase):
             owner=fsm, information_type=InformationType.USERDATA
         )
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=30), timedelta(days=1)
+            datetime.now(timezone.utc) - timedelta(days=30), timedelta(days=1)
         )
         login_person(fsm)
         self.factory.makeRevisionsForBranch(
diff --git a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
index 06c038b..fe6c2a5 100644
--- a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
+++ b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
@@ -4,12 +4,11 @@
 """Tests for the source package recipe view classes and templates."""
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from textwrap import dedent
 
 import transaction
 from fixtures import FakeLogger
-from pytz import UTC
 from testtools.matchers import Equals
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
@@ -1014,7 +1013,7 @@ class TestSourcePackageRecipeEditViewMixin:
 
     def test_edit_recipe_sets_date_last_modified(self):
         """Editing a recipe sets the date_last_modified property."""
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         recipe = self.makeRecipe(date_created=date_created)
 
         login_person(self.chef)
@@ -1375,11 +1374,11 @@ class TestSourcePackageRecipeViewMixin:
         )
         build.updateStatus(
             BuildStatus.BUILDING,
-            date_started=datetime(2010, 3, 16, tzinfo=UTC),
+            date_started=datetime(2010, 3, 16, tzinfo=timezone.utc),
         )
         build.updateStatus(
             BuildStatus.FULLYBUILT,
-            date_finished=datetime(2010, 3, 16, tzinfo=UTC),
+            date_finished=datetime(2010, 3, 16, tzinfo=timezone.utc),
         )
         return build
 
@@ -1488,11 +1487,11 @@ class TestSourcePackageRecipeViewMixin:
         binary_build.queueBuild()
         binary_build.updateStatus(
             BuildStatus.BUILDING,
-            date_started=datetime(2010, 4, 16, tzinfo=UTC),
+            date_started=datetime(2010, 4, 16, tzinfo=timezone.utc),
         )
         binary_build.updateStatus(
             BuildStatus.FULLYBUILT,
-            date_finished=datetime(2010, 4, 16, tzinfo=UTC),
+            date_finished=datetime(2010, 4, 16, tzinfo=timezone.utc),
         )
         binary_build.setLog(self.factory.makeLibraryFileAlias())
 
@@ -1573,7 +1572,7 @@ class TestSourcePackageRecipeViewMixin:
         # We create builds in time ascending order (oldest first) since we
         # use id as the ordering attribute and lower ids mean created earlier.
         date_gen = time_counter(
-            datetime(2010, 3, 16, tzinfo=UTC), timedelta(days=1)
+            datetime(2010, 3, 16, tzinfo=timezone.utc), timedelta(days=1)
         )
         build1 = self.makeBuildJob(recipe, next(date_gen))
         build2 = self.makeBuildJob(recipe, next(date_gen))
@@ -2016,7 +2015,9 @@ class TestSourcePackageRecipeBuildViewMixin:
         view = SourcePackageRecipeBuildView(build, None)
         self.assertIs(None, view.eta)
         queue_entry = removeSecurityProxy(build.queueBuild())
-        queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, UTC)
+        queue_entry._now = lambda: datetime(
+            1970, 1, 1, 0, 0, 0, 0, timezone.utc
+        )
         self.factory.makeBuilder(
             processors=[queue_entry.processor], virtualized=True
         )
@@ -2064,7 +2065,8 @@ class TestSourcePackageRecipeBuildViewMixin:
         self.makeBinaryBuild(release, "itanic")
         build = release.source_package_recipe_build
         build.updateStatus(
-            BuildStatus.BUILDING, date_started=datetime(2009, 1, 1, tzinfo=UTC)
+            BuildStatus.BUILDING,
+            date_started=datetime(2009, 1, 1, tzinfo=timezone.utc),
         )
         build.updateStatus(
             BuildStatus.FULLYBUILT,
diff --git a/lib/lp/code/doc/branch.rst b/lib/lp/code/doc/branch.rst
index 420594b..7984f04 100644
--- a/lib/lp/code/doc/branch.rst
+++ b/lib/lp/code/doc/branch.rst
@@ -186,10 +186,9 @@ branches.
 Now we create a few branches that we pretend were updated in a definite
 order.
 
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.testing import time_counter
-    >>> import pytz
-    >>> today = datetime.now(pytz.timezone("UTC"))
+    >>> today = datetime.now(timezone.utc)
     >>> product = factory.makeProduct(name="product")
     >>> user = factory.makePerson(name="user")
     >>> time_generator = time_counter()
diff --git a/lib/lp/code/doc/codeimport-job.rst b/lib/lp/code/doc/codeimport-job.rst
index e890cf5..6c131f1 100644
--- a/lib/lp/code/doc/codeimport-job.rst
+++ b/lib/lp/code/doc/codeimport-job.rst
@@ -101,8 +101,7 @@ is said to be overdue, and will be run as soon as possible.
 
 The CodeImportJob.isOverdue() method tells whether a job is overdue.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timezone
     >>> import_job = reviewed_import.import_job
 
     >>> from zope.security.proxy import removeSecurityProxy
@@ -114,14 +113,14 @@ The CodeImportJob.isOverdue() method tells whether a job is overdue.
 
 If date_due is in the future, then the job is not overdue.
 
-    >>> future_date = datetime(2100, 1, 1, tzinfo=UTC)
+    >>> future_date = datetime(2100, 1, 1, tzinfo=timezone.utc)
     >>> set_date_due(import_job, future_date)
     >>> import_job.isOverdue()
     False
 
 If date_due is in the past, then the job is overdue.
 
-    >>> past_date = datetime(1900, 1, 1, tzinfo=UTC)
+    >>> past_date = datetime(1900, 1, 1, tzinfo=timezone.utc)
     >>> set_date_due(import_job, past_date)
     >>> import_job.isOverdue()
     True
@@ -187,10 +186,8 @@ Requesting a job run
 When a job is pending, users can request that it be run as soon as
 possible.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
     >>> pending_job = reviewed_import.import_job
-    >>> future_date = datetime(2100, 1, 1, tzinfo=UTC)
+    >>> future_date = datetime(2100, 1, 1, tzinfo=timezone.utc)
 
 ICodeImportJob does not expose date_due, so we must use removeSecurityProxy.
 
@@ -260,7 +257,7 @@ current transaction time, we force a date in the a past into this
 field now so that we can check that updateHeartbeat has an effect.
 
     >>> removeSecurityProxy(running_job).heartbeat = datetime(
-    ...     2007, 1, 1, 0, 0, 0, tzinfo=UTC
+    ...     2007, 1, 1, 0, 0, 0, tzinfo=timezone.utc
     ... )
     >>> new_events = NewEvents()
 
diff --git a/lib/lp/code/doc/codeimport-result.rst b/lib/lp/code/doc/codeimport-result.rst
index 4135da7..d337c3b 100644
--- a/lib/lp/code/doc/codeimport-result.rst
+++ b/lib/lp/code/doc/codeimport-result.rst
@@ -47,10 +47,9 @@ it.
 Then create a result object.
 
     >>> from lp.testing import time_counter
-    >>> from pytz import UTC
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> time_source = time_counter(
-    ...     datetime(2008, 1, 1, tzinfo=UTC), timedelta(days=1)
+    ...     datetime(2008, 1, 1, tzinfo=timezone.utc), timedelta(days=1)
     ... )
     >>> odin = factory.makeCodeImportMachine(hostname="odin")
     >>> from lp.code.enums import CodeImportResultStatus
diff --git a/lib/lp/code/doc/revision.rst b/lib/lp/code/doc/revision.rst
index d99d0ee..a16ab5d 100644
--- a/lib/lp/code/doc/revision.rst
+++ b/lib/lp/code/doc/revision.rst
@@ -68,9 +68,8 @@ on the Arch revision id.
 The revision_date is the commit date recorded by the revision control system,
 while the date_created is the time when the database record was created.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
-    >>> date = datetime(2005, 3, 8, 12, 0, tzinfo=UTC)
+    >>> from datetime import datetime, timezone
+    >>> date = datetime(2005, 3, 8, 12, 0, tzinfo=timezone.utc)
     >>> revision_1 = Revision(
     ...     log_body=log_body_1,
     ...     revision_author=author,
diff --git a/lib/lp/code/feed/tests/test_revision.py b/lib/lp/code/feed/tests/test_revision.py
index a2ed1a6..aa7eff5 100644
--- a/lib/lp/code/feed/tests/test_revision.py
+++ b/lib/lp/code/feed/tests/test_revision.py
@@ -3,9 +3,8 @@
 
 """Tests for the revision feeds."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import UTC
 from zope.component import getUtility
 
 from lp.code.feed.branch import (
@@ -27,7 +26,7 @@ class TestRevisionFeedId(TestCaseWithFactory):
     def test_format(self):
         # The id contains the iso format of the date part of the revision
         # date, and the revision id.
-        revision_date = datetime(2009, 7, 21, 12, tzinfo=UTC)
+        revision_date = datetime(2009, 7, 21, 12, tzinfo=timezone.utc)
         revision = self.factory.makeRevision(
             revision_date=revision_date, rev_id="test_revision_id"
         )
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index 93b75cf..b9cfedc 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -8,10 +8,9 @@ __all__ = [
 ]
 
 import operator
-from datetime import datetime
+from datetime import datetime, timezone
 from functools import partial
 
-import pytz
 from breezy import urlutils
 from breezy.revision import NULL_REVISION
 from breezy.url_policy_open import open_only_scheme
@@ -1548,9 +1547,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
         ):
             # No mirror was requested since we started mirroring.
             increment = getUtility(IBranchPuller).MIRROR_TIME_INCREMENT
-            self.next_mirror_time = (
-                datetime.now(pytz.timezone("UTC")) + increment
-            )
+            self.next_mirror_time = datetime.now(timezone.utc) + increment
         if isinstance(last_revision_id, bytes):
             last_revision_id = last_revision_id.decode("ASCII")
         self.last_mirrored_id = last_revision_id
@@ -1577,7 +1574,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
             and self.mirror_failures < max_failures
         ):
             self.next_mirror_time = datetime.now(
-                pytz.timezone("UTC")
+                timezone.utc
             ) + increment * 2 ** (self.mirror_failures - 1)
 
     def _deleteBranchSubscriptions(self):
diff --git a/lib/lp/code/model/branchcloud.py b/lib/lp/code/model/branchcloud.py
index 22a850b..55fe294 100644
--- a/lib/lp/code/model/branchcloud.py
+++ b/lib/lp/code/model/branchcloud.py
@@ -8,9 +8,8 @@ __all__ = [
 ]
 
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.expr import Alias, Func
 from storm.locals import Count, Desc, Max, Not
 from zope.interface import provider
@@ -32,7 +31,7 @@ class BranchCloud:
             "distinct", RevisionCache.revision_author_id
         )
         commits = Alias(Count(RevisionCache.revision_id))
-        epoch = datetime.now(pytz.UTC) - timedelta(days=30)
+        epoch = datetime.now(timezone.utc) - timedelta(days=30)
         # It doesn't matter if this query is even a whole day out of date, so
         # use the standby store.
         result = IStandbyStore(RevisionCache).find(
diff --git a/lib/lp/code/model/branchmergeproposaljob.py b/lib/lp/code/model/branchmergeproposaljob.py
index 7019c54..e173f29 100644
--- a/lib/lp/code/model/branchmergeproposaljob.py
+++ b/lib/lp/code/model/branchmergeproposaljob.py
@@ -21,9 +21,8 @@ __all__ = [
 
 import json
 from contextlib import ExitStack
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import six
 from lazr.delegates import delegate_to
 from lazr.enum import DBEnumeratedType, DBItem
@@ -702,11 +701,11 @@ class BranchMergeProposalJobSource(BaseRunnableJobSource):
                 job.status == JobStatus.RUNNING
                 or (
                     job.lease_expires is not None
-                    and job.lease_expires >= datetime.now(pytz.UTC)
+                    and job.lease_expires >= datetime.now(timezone.utc)
                 )
                 or (
                     job.scheduled_start is not None
-                    and job.scheduled_start > datetime.now(pytz.UTC)
+                    and job.scheduled_start > datetime.now(timezone.utc)
                 )
             ):
                 continue
@@ -721,7 +720,7 @@ class BranchMergeProposalJobSource(BaseRunnableJobSource):
                     minutes = (
                         config.codehosting.update_preview_diff_ready_timeout
                     )
-                    cut_off_time = datetime.now(pytz.UTC) - timedelta(
+                    cut_off_time = datetime.now(timezone.utc) - timedelta(
                         minutes=minutes
                     )
                     if job.date_created > cut_off_time:
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index c06e7a6..23cb47a 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -8,10 +8,9 @@ __all__ = [
 ]
 
 from copy import copy
-from datetime import timedelta
+from datetime import timedelta, timezone
 from operator import itemgetter
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.databases.postgres import JSON
 from storm.locals import Bool, DateTime, Desc, Int, Reference, Store, Unicode
@@ -189,16 +188,16 @@ class CIBuild(PackageBuildMixin, StormBase):
     builder_constraints = JSON(name="builder_constraints", allow_none=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_started = DateTime(
-        name="date_started", tzinfo=pytz.UTC, allow_none=True
+        name="date_started", tzinfo=timezone.utc, allow_none=True
     )
     date_finished = DateTime(
-        name="date_finished", tzinfo=pytz.UTC, allow_none=True
+        name="date_finished", tzinfo=timezone.utc, allow_none=True
     )
     date_first_dispatched = DateTime(
-        name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True
+        name="date_first_dispatched", tzinfo=timezone.utc, allow_none=True
     )
 
     builder_id = Int(name="builder", allow_none=True)
diff --git a/lib/lp/code/model/codeimport.py b/lib/lp/code/model/codeimport.py
index 5be6617..185888f 100644
--- a/lib/lp/code/model/codeimport.py
+++ b/lib/lp/code/model/codeimport.py
@@ -8,9 +8,8 @@ __all__ = [
     "CodeImportSet",
 ]
 
-from datetime import timedelta
+from datetime import timedelta, timezone
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.expr import And, Desc, Func, Select
 from storm.locals import (
@@ -101,7 +100,9 @@ class CodeImport(StormBase):
         self.cvs_root = cvs_root
         self.cvs_module = cvs_module
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
     branch_id = Int(name="branch", allow_none=True)
     branch = Reference(branch_id, "Branch.id")
     git_repository_id = Int(name="git_repository", allow_none=True)
@@ -154,7 +155,7 @@ class CodeImport(StormBase):
 
     url = Unicode(default=None)
 
-    date_last_successful = DateTime(tzinfo=pytz.UTC, default=None)
+    date_last_successful = DateTime(tzinfo=timezone.utc, default=None)
     update_interval = TimeDelta(default=None)
 
     @property
diff --git a/lib/lp/code/model/codeimportevent.py b/lib/lp/code/model/codeimportevent.py
index a098246..89f14f2 100644
--- a/lib/lp/code/model/codeimportevent.py
+++ b/lib/lp/code/model/codeimportevent.py
@@ -9,8 +9,8 @@ __all__ = [
     "CodeImportEventToken",
 ]
 
+from datetime import timezone
 
-import pytz
 from lazr.enum import DBItem
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
@@ -41,7 +41,9 @@ class CodeImportEvent(StormBase):
 
     id = Int(primary=True)
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
 
     event_type = DBEnum(
         name="entry_type", enum=CodeImportEventType, allow_none=False
diff --git a/lib/lp/code/model/codeimportjob.py b/lib/lp/code/model/codeimportjob.py
index 27876a6..76b8344 100644
--- a/lib/lp/code/model/codeimportjob.py
+++ b/lib/lp/code/model/codeimportjob.py
@@ -9,9 +9,8 @@ __all__ = [
     "CodeImportJobWorkflow",
 ]
 
-from datetime import timedelta
+from datetime import timedelta, timezone
 
-import pytz
 from storm.expr import Cast
 from storm.locals import DateTime, Desc, Int, Reference, Store, Unicode
 from zope.component import getUtility
@@ -63,7 +62,9 @@ class CodeImportJob(StormBase):
 
     id = Int(primary=True)
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     code_import_id = Int(name="code_import", allow_none=False)
     code_import = Reference(code_import_id, "CodeImport.id")
@@ -71,7 +72,7 @@ class CodeImportJob(StormBase):
     machine_id = Int(name="machine", allow_none=True, default=None)
     machine = Reference(machine_id, "CodeImportMachine.id")
 
-    date_due = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    date_due = DateTime(tzinfo=timezone.utc, allow_none=False)
 
     state = DBEnum(
         enum=CodeImportJobState,
@@ -89,11 +90,11 @@ class CodeImportJob(StormBase):
 
     ordering = Int(allow_none=True, default=None)
 
-    heartbeat = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    heartbeat = DateTime(tzinfo=timezone.utc, allow_none=True, default=None)
 
     logtail = Unicode(allow_none=True, default=None)
 
-    date_started = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_started = DateTime(tzinfo=timezone.utc, allow_none=True, default=None)
 
     def __init__(self, code_import, date_due):
         super().__init__()
diff --git a/lib/lp/code/model/codeimportmachine.py b/lib/lp/code/model/codeimportmachine.py
index 123cbf3..1a191b7 100644
--- a/lib/lp/code/model/codeimportmachine.py
+++ b/lib/lp/code/model/codeimportmachine.py
@@ -8,7 +8,8 @@ __all__ = [
     "CodeImportMachineSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Desc, Int, ReferenceSet, Unicode
 from zope.component import getUtility
 from zope.interface import implementer
@@ -37,7 +38,9 @@ class CodeImportMachine(StormBase):
 
     id = Int(primary=True)
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
 
     hostname = Unicode(allow_none=False)
     state = DBEnum(
@@ -45,7 +48,7 @@ class CodeImportMachine(StormBase):
         allow_none=False,
         default=CodeImportMachineState.OFFLINE,
     )
-    heartbeat = DateTime(tzinfo=pytz.UTC, allow_none=True)
+    heartbeat = DateTime(tzinfo=timezone.utc, allow_none=True)
 
     current_jobs = ReferenceSet(
         id,
diff --git a/lib/lp/code/model/codeimportresult.py b/lib/lp/code/model/codeimportresult.py
index 2a3126f..d089233 100644
--- a/lib/lp/code/model/codeimportresult.py
+++ b/lib/lp/code/model/codeimportresult.py
@@ -5,7 +5,8 @@
 
 __all__ = ["CodeImportResult", "CodeImportResultSet"]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
 
@@ -29,7 +30,9 @@ class CodeImportResult(StormBase):
 
     id = Int(primary=True)
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     code_import_id = Int(name="code_import", allow_none=False)
     code_import = Reference(code_import_id, "CodeImport.id")
@@ -52,7 +55,7 @@ class CodeImportResult(StormBase):
 
     status = DBEnum(enum=CodeImportResultStatus, allow_none=False)
 
-    date_job_started = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    date_job_started = DateTime(tzinfo=timezone.utc, allow_none=False)
 
     def __init__(
         self,
diff --git a/lib/lp/code/model/codereviewvote.py b/lib/lp/code/model/codereviewvote.py
index 2dcd1ab..e423516 100644
--- a/lib/lp/code/model/codereviewvote.py
+++ b/lib/lp/code/model/codereviewvote.py
@@ -7,7 +7,8 @@ __all__ = [
     "CodeReviewVoteReference",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Store, Unicode
 from zope.interface import implementer
 
@@ -34,7 +35,9 @@ class CodeReviewVoteReference(StormBase):
     branch_merge_proposal = Reference(
         branch_merge_proposal_id, "BranchMergeProposal.id"
     )
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
     registrant_id = Int(name="registrant", allow_none=False)
     registrant = Reference(registrant_id, "Person.id")
     reviewer_id = Int(name="reviewer", allow_none=False)
diff --git a/lib/lp/code/model/gitactivity.py b/lib/lp/code/model/gitactivity.py
index 3cb588d..75e26cd 100644
--- a/lib/lp/code/model/gitactivity.py
+++ b/lib/lp/code/model/gitactivity.py
@@ -7,7 +7,8 @@ __all__ = [
     "GitActivity",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import JSON, DateTime, Int, Reference
 from zope.interface import implementer
 
@@ -34,7 +35,7 @@ class GitActivity(StormBase):
     repository = Reference(repository_id, "GitRepository.id")
 
     date_changed = DateTime(
-        name="date_changed", tzinfo=pytz.UTC, allow_none=False
+        name="date_changed", tzinfo=timezone.utc, allow_none=False
     )
 
     changer_id = Int(
diff --git a/lib/lp/code/model/gitref.py b/lib/lp/code/model/gitref.py
index 60ef93f..240e604 100644
--- a/lib/lp/code/model/gitref.py
+++ b/lib/lp/code/model/gitref.py
@@ -9,10 +9,10 @@ __all__ = [
 ]
 
 import re
+from datetime import timezone
 from functools import partial
 from urllib.parse import quote, quote_plus, urlsplit
 
-import pytz
 import requests
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.expr import And, Or
@@ -615,13 +615,13 @@ class GitRef(GitRefMixin, StormBase):
     author_id = Int(name="author", allow_none=True)
     author = Reference(author_id, "RevisionAuthor.id")
     author_date = DateTime(
-        name="author_date", tzinfo=pytz.UTC, allow_none=True
+        name="author_date", tzinfo=timezone.utc, allow_none=True
     )
 
     committer_id = Int(name="committer", allow_none=True)
     committer = Reference(committer_id, "RevisionAuthor.id")
     committer_date = DateTime(
-        name="committer_date", tzinfo=pytz.UTC, allow_none=True
+        name="committer_date", tzinfo=timezone.utc, allow_none=True
     )
 
     commit_message = Unicode(name="commit_message", allow_none=True)
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 27707ad..8021ac4 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -11,14 +11,13 @@ __all__ = [
 import email
 import logging
 from collections import OrderedDict, defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from fnmatch import fnmatch
 from functools import partial
 from itertools import chain, groupby
 from operator import attrgetter
 from urllib.parse import quote_plus, urlsplit, urlunsplit
 
-import pytz
 import six
 from breezy import urlutils
 from lazr.enum import DBItem
@@ -192,7 +191,7 @@ def parse_git_commits(commits):
         if author is not None:
             if "time" in author:
                 info["author_date"] = datetime.fromtimestamp(
-                    author["time"], tz=pytz.UTC
+                    author["time"], tz=timezone.utc
                 )
             if "name" in author and "email" in author:
                 try:
@@ -211,7 +210,7 @@ def parse_git_commits(commits):
         if committer is not None:
             if "time" in committer:
                 info["committer_date"] = datetime.fromtimestamp(
-                    committer["time"], tz=pytz.UTC
+                    committer["time"], tz=timezone.utc
                 )
             if "name" in committer and "email" in committer:
                 try:
@@ -266,10 +265,10 @@ class GitRepository(
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     repository_type = DBEnum(
@@ -318,10 +317,10 @@ class GitRepository(
     pack_count = Int(name="pack_count", allow_none=True)
 
     date_last_repacked = DateTime(
-        name="date_last_repacked", tzinfo=pytz.UTC, allow_none=True
+        name="date_last_repacked", tzinfo=timezone.utc, allow_none=True
     )
     date_last_scanned = DateTime(
-        name="date_last_scanned", tzinfo=pytz.UTC, allow_none=True
+        name="date_last_scanned", tzinfo=timezone.utc, allow_none=True
     )
 
     builder_constraints = ImmutablePgJSON(
@@ -2552,7 +2551,7 @@ class GitRepositoryMacaroonIssuer(MacaroonIssuerBase):
         try:
             expires = datetime.strptime(
                 caveat_value, self._timestamp_format
-            ).replace(tzinfo=pytz.UTC)
+            ).replace(tzinfo=timezone.utc)
         except ValueError:
             return False
         store = IStore(GitRepository)
diff --git a/lib/lp/code/model/gitrule.py b/lib/lp/code/model/gitrule.py
index 1848ec4..397f451 100644
--- a/lib/lp/code/model/gitrule.py
+++ b/lib/lp/code/model/gitrule.py
@@ -9,8 +9,8 @@ __all__ = [
 ]
 
 from collections import OrderedDict, defaultdict
+from datetime import timezone
 
-import pytz
 from lazr.enum import DBItem
 from lazr.restful.interfaces import IFieldMarshaller, IJSONPublishable
 from lazr.restful.utils import get_current_browser_request
@@ -78,10 +78,10 @@ class GitRule(StormBase):
     creator = Reference(creator_id, "Person.id")
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     def __init__(
@@ -299,10 +299,10 @@ class GitRuleGrant(StormBase, GitRuleGrantMixin):
     grantor = Reference(grantor_id, "Person.id")
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     def __init__(
diff --git a/lib/lp/code/model/revision.py b/lib/lp/code/model/revision.py
index 10dcd28..4d7f7ab 100644
--- a/lib/lp/code/model/revision.py
+++ b/lib/lp/code/model/revision.py
@@ -11,10 +11,9 @@ __all__ = [
 ]
 
 import email
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import itemgetter
 
-import pytz
 from breezy.revision import NULL_REVISION
 from storm.expr import And, Asc, Desc, Join, Or, Select
 from storm.locals import (
@@ -64,7 +63,10 @@ class Revision(StormBase):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", allow_none=False, default=DEFAULT, tzinfo=pytz.UTC
+        name="date_created",
+        allow_none=False,
+        default=DEFAULT,
+        tzinfo=timezone.utc,
     )
     log_body = Unicode(name="log_body", allow_none=False)
 
@@ -73,7 +75,7 @@ class Revision(StormBase):
 
     revision_id = Unicode(name="revision_id", allow_none=False)
     revision_date = DateTime(
-        name="revision_date", allow_none=True, tzinfo=pytz.UTC
+        name="revision_date", allow_none=True, tzinfo=timezone.utc
     )
 
     karma_allocated = Bool(
@@ -382,9 +384,8 @@ class RevisionSet:
         """
         # Work around Python bug #1646728.
         # See https://launchpad.net/bugs/81544.
-        UTC = pytz.timezone("UTC")
         int_timestamp = int(timestamp)
-        revision_date = datetime.fromtimestamp(int_timestamp, tz=UTC)
+        revision_date = datetime.fromtimestamp(int_timestamp, tz=timezone.utc)
         revision_date += timedelta(seconds=timestamp - int_timestamp)
         return revision_date
 
@@ -693,7 +694,7 @@ class RevisionSet:
         # Storm doesn't handle remove a limited result set:
         #    FeatureError: Can't remove a sliced result set
         store = IPrimaryStore(RevisionCache)
-        epoch = datetime.now(tz=pytz.UTC) - timedelta(days=30)
+        epoch = datetime.now(tz=timezone.utc) - timedelta(days=30)
         subquery = Select(
             [RevisionCache.id],
             RevisionCache.revision_date < epoch,
@@ -704,7 +705,7 @@ class RevisionSet:
 
 def revision_time_limit(day_limit):
     """The storm fragment to limit the revision_date field of the Revision."""
-    now = datetime.now(pytz.UTC)
+    now = datetime.now(timezone.utc)
     earliest = now - timedelta(days=day_limit)
 
     return And(
@@ -725,7 +726,7 @@ class RevisionCache(StormBase):
     revision_author_id = Int(name="revision_author", allow_none=False)
     revision_author = Reference(revision_author_id, "RevisionAuthor.id")
 
-    revision_date = DateTime(allow_none=False, tzinfo=pytz.UTC)
+    revision_date = DateTime(allow_none=False, tzinfo=timezone.utc)
 
     product_id = Int(name="product", allow_none=True)
     product = Reference(product_id, "Product.id")
diff --git a/lib/lp/code/model/revisioncache.py b/lib/lp/code/model/revisioncache.py
index 4d261bf..9ad12ed 100644
--- a/lib/lp/code/model/revisioncache.py
+++ b/lib/lp/code/model/revisioncache.py
@@ -7,9 +7,8 @@ __all__ = [
     "GenericRevisionCollection",
 ]
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.expr import SQL, Desc, Func
 from zope.interface import implementer
 
@@ -28,7 +27,7 @@ class GenericRevisionCollection:
     def __init__(self, store=None, filter_expressions=None):
         self._store = store
         if filter_expressions is None:
-            epoch = datetime.now(pytz.UTC) - timedelta(days=30)
+            epoch = datetime.now(timezone.utc) - timedelta(days=30)
             filter_expressions = [RevisionCache.revision_date >= epoch]
         self._filter_expressions = filter_expressions
 
diff --git a/lib/lp/code/model/revisionstatus.py b/lib/lp/code/model/revisionstatus.py
index 518c5a8..dbc910a 100644
--- a/lib/lp/code/model/revisionstatus.py
+++ b/lib/lp/code/model/revisionstatus.py
@@ -9,8 +9,8 @@ __all__ = [
 
 import io
 import os
+from datetime import timezone
 
-import pytz
 from storm.databases.postgres import JSON
 from storm.expr import Desc
 from storm.locals import And, DateTime, Int, Reference, Unicode
@@ -61,14 +61,14 @@ class RevisionStatusReport(StormBase):
     ci_build = Reference(ci_build_id, "CIBuild.id")
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
 
     date_started = DateTime(
-        name="date_started", tzinfo=pytz.UTC, allow_none=True
+        name="date_started", tzinfo=timezone.utc, allow_none=True
     )
     date_finished = DateTime(
-        name="date_finished", tzinfo=pytz.UTC, allow_none=True
+        name="date_finished", tzinfo=timezone.utc, allow_none=True
     )
 
     properties = JSON("properties", allow_none=True)
@@ -300,7 +300,7 @@ class RevisionStatusArtifact(StormBase):
     )
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=True
+        name="date_created", tzinfo=timezone.utc, allow_none=True
     )
 
     def __init__(
diff --git a/lib/lp/code/model/seriessourcepackagebranch.py b/lib/lp/code/model/seriessourcepackagebranch.py
index da5327a..85e787a 100644
--- a/lib/lp/code/model/seriessourcepackagebranch.py
+++ b/lib/lp/code/model/seriessourcepackagebranch.py
@@ -8,9 +8,8 @@ __all__ = [
     "SeriesSourcePackageBranchSet",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.locals import DateTime, Int, Reference
 from zope.interface import implementer
 
@@ -93,7 +92,7 @@ class SeriesSourcePackageBranchSet:
         )
 
         if date_created is None:
-            date_created = datetime.now(pytz.UTC)
+            date_created = datetime.now(timezone.utc)
         sspb = SeriesSourcePackageBranch(
             distroseries,
             pocket,
diff --git a/lib/lp/code/model/sourcepackagerecipe.py b/lib/lp/code/model/sourcepackagerecipe.py
index de53ee2..937024a 100644
--- a/lib/lp/code/model/sourcepackagerecipe.py
+++ b/lib/lp/code/model/sourcepackagerecipe.py
@@ -8,11 +8,10 @@ __all__ = [
 ]
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
 from lazr.delegates import delegate_to
-from pytz import utc
 from storm.expr import And, LeftJoin
 from storm.locals import (
     Bool,
@@ -235,7 +234,9 @@ class SourcePackageRecipe(StormBase):
 
     @staticmethod
     def findStaleDailyBuilds():
-        one_day_ago = datetime.now(utc) - timedelta(hours=23, minutes=50)
+        one_day_ago = datetime.now(timezone.utc) - timedelta(
+            hours=23, minutes=50
+        )
         joins = (
             SourcePackageRecipe,
             LeftJoin(
diff --git a/lib/lp/code/model/sourcepackagerecipebuild.py b/lib/lp/code/model/sourcepackagerecipebuild.py
index eb600f6..4f95e4e 100644
--- a/lib/lp/code/model/sourcepackagerecipebuild.py
+++ b/lib/lp/code/model/sourcepackagerecipebuild.py
@@ -8,9 +8,8 @@ __all__ = [
 ]
 
 import logging
-from datetime import timedelta
+from datetime import timedelta, timezone
 
-import pytz
 from psycopg2 import ProgrammingError
 from storm.locals import Bool, DateTime, Int, Reference, Unicode
 from storm.store import EmptyResultSet, Store
@@ -117,12 +116,12 @@ class SourcePackageRecipeBuild(
     virtualized = Bool(name="virtualized")
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
-    date_started = DateTime(name="date_started", tzinfo=pytz.UTC)
-    date_finished = DateTime(name="date_finished", tzinfo=pytz.UTC)
+    date_started = DateTime(name="date_started", tzinfo=timezone.utc)
+    date_finished = DateTime(name="date_finished", tzinfo=timezone.utc)
     date_first_dispatched = DateTime(
-        name="date_first_dispatched", tzinfo=pytz.UTC
+        name="date_first_dispatched", tzinfo=timezone.utc
     )
 
     builder_id = Int(name="builder")
diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
index dfb0983..13da173 100644
--- a/lib/lp/code/model/tests/test_branch.py
+++ b/lib/lp/code/model/tests/test_branch.py
@@ -3,14 +3,13 @@
 
 """Tests for Branches."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import transaction
 from breezy.branch import Branch
 from breezy.bzr.bzrdir import BzrDir
 from breezy.revision import NULL_REVISION
 from breezy.url_policy_open import BadUrl
-from pytz import UTC
 from storm.exceptions import LostObjectError
 from storm.locals import Store
 from testscenarios import WithScenarios, load_tests_apply_scenarios
@@ -374,7 +373,7 @@ class TestBranchWriteJobViaCelery(TestCaseWithFactory):
         job = store.find(
             BranchJob, BranchJob.job_type == BranchJobType.RECLAIM_BRANCH_SPACE
         ).one()
-        job.job.scheduled_start = datetime.now(UTC)
+        job.job.scheduled_start = datetime.now(timezone.utc)
         with block_on_job():
             transaction.commit()
         self.assertThat(branch_path, Not(PathExists()))
@@ -2276,7 +2275,7 @@ class BranchDateLastModified(TestCaseWithFactory):
 
     def test_bugBranchLinkUpdates(self):
         """Linking a branch to a bug updates the last modified time."""
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         branch = self.factory.makeAnyBranch(date_created=date_created)
         self.assertEqual(branch.date_last_modified, date_created)
 
@@ -2298,7 +2297,7 @@ class BranchDateLastModified(TestCaseWithFactory):
         # If updateScannedDetails is called with a null revision, it
         # effectively means that there is an empty branch, so we can't use the
         # revision date, so we set the last modified time to UTC_NOW.
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         branch = self.factory.makeAnyBranch(date_created=date_created)
         branch.updateScannedDetails(None, 0)
         self.assertSqlAttributeEqualsDate(
@@ -2310,9 +2309,9 @@ class BranchDateLastModified(TestCaseWithFactory):
         # revision date set in the past (the usual case), the last modified
         # time of the branch is set to be the date from the Bazaar revision
         # (Revision.revision_date).
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         branch = self.factory.makeAnyBranch(date_created=date_created)
-        revision_date = datetime(2005, 2, 2, 12, tzinfo=UTC)
+        revision_date = datetime(2005, 2, 2, 12, tzinfo=timezone.utc)
         revision = self.factory.makeRevision(revision_date=revision_date)
         branch.updateScannedDetails(revision, 1)
         self.assertEqual(revision_date, branch.date_last_modified)
@@ -2320,10 +2319,10 @@ class BranchDateLastModified(TestCaseWithFactory):
     def test_updateScannedDetails_with_future_revision(self):
         # If updateScannedDetails is called with a revision with which has a
         # revision date set in the future, UTC_NOW is used as the last modified
-        # time.  date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        # time.  date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         branch = self.factory.makeAnyBranch(date_created=date_created)
-        revision_date = datetime.now(UTC) + timedelta(days=1000)
+        revision_date = datetime.now(timezone.utc) + timedelta(days=1000)
         revision = self.factory.makeRevision(revision_date=revision_date)
         branch.updateScannedDetails(revision, 1)
         self.assertSqlAttributeEqualsDate(
@@ -3566,7 +3565,7 @@ class TestBranchGetMainlineBranchRevisions(TestCaseWithFactory):
     def test_start_date(self):
         # Revisions created before the start date are not returned.
         branch = self.factory.makeAnyBranch()
-        epoch = datetime(2009, 9, 10, tzinfo=UTC)
+        epoch = datetime(2009, 9, 10, tzinfo=timezone.utc)
         # Add some revisions before the epoch.
         add_revision_to_branch(self.factory, branch, epoch - timedelta(days=1))
         new = add_revision_to_branch(
@@ -3579,7 +3578,7 @@ class TestBranchGetMainlineBranchRevisions(TestCaseWithFactory):
     def test_end_date(self):
         # Revisions created after the end date are not returned.
         branch = self.factory.makeAnyBranch()
-        epoch = datetime(2009, 9, 10, tzinfo=UTC)
+        epoch = datetime(2009, 9, 10, tzinfo=timezone.utc)
         end_date = epoch + timedelta(days=2)
         in_range = add_revision_to_branch(
             self.factory, branch, end_date - timedelta(days=1)
@@ -3595,7 +3594,7 @@ class TestBranchGetMainlineBranchRevisions(TestCaseWithFactory):
     def test_newest_first(self):
         # If oldest_first is False, the newest are returned first.
         branch = self.factory.makeAnyBranch()
-        epoch = datetime(2009, 9, 10, tzinfo=UTC)
+        epoch = datetime(2009, 9, 10, tzinfo=timezone.utc)
         old = add_revision_to_branch(
             self.factory, branch, epoch + timedelta(days=1)
         )
@@ -3609,7 +3608,7 @@ class TestBranchGetMainlineBranchRevisions(TestCaseWithFactory):
     def test_oldest_first(self):
         # If oldest_first is True, the oldest are returned first.
         branch = self.factory.makeAnyBranch()
-        epoch = datetime(2009, 9, 10, tzinfo=UTC)
+        epoch = datetime(2009, 9, 10, tzinfo=timezone.utc)
         old = add_revision_to_branch(
             self.factory, branch, epoch + timedelta(days=1)
         )
@@ -3623,7 +3622,7 @@ class TestBranchGetMainlineBranchRevisions(TestCaseWithFactory):
     def test_only_mainline_revisions(self):
         # Only mainline revisions are returned.
         branch = self.factory.makeAnyBranch()
-        epoch = datetime(2009, 9, 10, tzinfo=UTC)
+        epoch = datetime(2009, 9, 10, tzinfo=timezone.utc)
         old = add_revision_to_branch(
             self.factory, branch, epoch + timedelta(days=1)
         )
@@ -3795,7 +3794,7 @@ class TestBranchUnscan(TestCaseWithFactory):
             branch.unscan()
 
     def test_getLatestScanJob(self):
-        complete_date = datetime.now(UTC)
+        complete_date = datetime.now(timezone.utc)
 
         branch = self.factory.makeAnyBranch()
         failed_job = BranchScanJob.create(branch)
@@ -3813,7 +3812,7 @@ class TestBranchUnscan(TestCaseWithFactory):
         self.assertIsNone(result)
 
     def test_getLatestScanJob_correct_branch(self):
-        complete_date = datetime.now(UTC)
+        complete_date = datetime.now(timezone.utc)
 
         main_branch = self.factory.makeAnyBranch()
         second_branch = self.factory.makeAnyBranch()
diff --git a/lib/lp/code/model/tests/test_branchcloud.py b/lib/lp/code/model/tests/test_branchcloud.py
index 06401b6..0bb0745 100644
--- a/lib/lp/code/model/tests/test_branchcloud.py
+++ b/lib/lp/code/model/tests/test_branchcloud.py
@@ -3,9 +3,8 @@
 
 """Tests for IBranchCloud provider."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from storm.locals import Store
 from zope.component import getUtility
@@ -42,7 +41,7 @@ class TestBranchCloud(TestCaseWithFactory):
             # type that it is aggregating, the last commit time is not
             # timezone-aware.  Whack the UTC timezone on it here for
             # easier comparing in the tests.
-            return value.replace(tzinfo=pytz.UTC)
+            return value.replace(tzinfo=timezone.utc)
 
         return [
             (name, commits, authors, add_utc(last_commit))
@@ -63,7 +62,7 @@ class TestBranchCloud(TestCaseWithFactory):
         if last_commit_date is None:
             # By default we create revisions that are within the last 30 days.
             date_generator = time_counter(
-                datetime.now(pytz.UTC) - timedelta(days=25), delta
+                datetime.now(timezone.utc) - timedelta(days=25), delta
             )
         else:
             start_date = last_commit_date - delta * (revision_count - 1)
@@ -101,7 +100,7 @@ class TestBranchCloud(TestCaseWithFactory):
 
     def test_revisions_counted(self):
         # getProductsWithInfo includes products that public revisions.
-        last_commit_date = datetime.now(pytz.UTC) - timedelta(days=5)
+        last_commit_date = datetime.now(timezone.utc) - timedelta(days=5)
         product = self.factory.makeProduct()
         self.makeBranch(product=product, last_commit_date=last_commit_date)
         self.assertEqual(
@@ -114,7 +113,7 @@ class TestBranchCloud(TestCaseWithFactory):
         # over 30 days old, we don't count them.
         product = self.factory.makeProduct()
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=33),
+            datetime.now(timezone.utc) - timedelta(days=33),
             delta=timedelta(days=2),
         )
         store = Store.of(product)
diff --git a/lib/lp/code/model/tests/test_branchcollection.py b/lib/lp/code/model/tests/test_branchcollection.py
index a871cc5..da8a351 100644
--- a/lib/lp/code/model/tests/test_branchcollection.py
+++ b/lib/lp/code/model/tests/test_branchcollection.py
@@ -3,10 +3,9 @@
 
 """Tests for branch collections."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-import pytz
 from storm.expr import Asc, Desc
 from storm.store import EmptyResultSet, Store
 from testtools.matchers import Equals
@@ -640,11 +639,15 @@ class TestBranchCollectionFilters(TestCaseWithFactory):
     def test_modifiedSince(self):
         # Only branches modified since the time specified will be returned.
         old_branch = self.factory.makeAnyBranch()
-        old_branch.date_last_modified = datetime(2008, 1, 1, tzinfo=pytz.UTC)
+        old_branch.date_last_modified = datetime(
+            2008, 1, 1, tzinfo=timezone.utc
+        )
         new_branch = self.factory.makeAnyBranch()
-        new_branch.date_last_modified = datetime(2009, 1, 1, tzinfo=pytz.UTC)
+        new_branch.date_last_modified = datetime(
+            2009, 1, 1, tzinfo=timezone.utc
+        )
         branches = self.all_branches.modifiedSince(
-            datetime(2008, 6, 1, tzinfo=pytz.UTC)
+            datetime(2008, 6, 1, tzinfo=timezone.utc)
         )
         self.assertEqual([new_branch], list(branches.getBranches()))
 
@@ -652,14 +655,14 @@ class TestBranchCollectionFilters(TestCaseWithFactory):
         # Only branches scanned since the time specified will be returned.
         old_branch = self.factory.makeAnyBranch()
         removeSecurityProxy(old_branch).last_scanned = datetime(
-            2008, 1, 1, tzinfo=pytz.UTC
+            2008, 1, 1, tzinfo=timezone.utc
         )
         new_branch = self.factory.makeAnyBranch()
         removeSecurityProxy(new_branch).last_scanned = datetime(
-            2009, 1, 1, tzinfo=pytz.UTC
+            2009, 1, 1, tzinfo=timezone.utc
         )
         branches = self.all_branches.scannedSince(
-            datetime(2008, 6, 1, tzinfo=pytz.UTC)
+            datetime(2008, 6, 1, tzinfo=timezone.utc)
         )
         self.assertEqual([new_branch], list(branches.getBranches()))
 
@@ -1041,7 +1044,7 @@ class TestBranchMergeProposals(TestCaseWithFactory):
         bmp2 = self.factory.makeBranchMergeProposal(
             target_branch=target, source_branch=branch2
         )
-        old_date = datetime.now(pytz.UTC) - timedelta(hours=1)
+        old_date = datetime.now(timezone.utc) - timedelta(hours=1)
         self.factory.makePreviewDiff(
             merge_proposal=bmp1, date_created=old_date
         )
diff --git a/lib/lp/code/model/tests/test_branchjob.py b/lib/lp/code/model/tests/test_branchjob.py
index 2f66299..8d54783 100644
--- a/lib/lp/code/model/tests/test_branchjob.py
+++ b/lib/lp/code/model/tests/test_branchjob.py
@@ -3,12 +3,11 @@
 
 """Tests for BranchJobs."""
 
-import datetime
 import os
 import shutil
+from datetime import datetime, timedelta, timezone
 from typing import Optional
 
-import pytz
 import transaction
 from breezy import errors as bzr_errors
 from breezy.branch import Branch
@@ -638,7 +637,7 @@ class TestRevisionsAddedJob(TestCaseWithFactory):
         """
         self.useBzrBranches(direct_database=True)
         target_branch, tree = self.create_branch_and_tree("tree")
-        the_past = datetime.datetime(2009, 1, 1, tzinfo=pytz.UTC)
+        the_past = datetime(2009, 1, 1, tzinfo=timezone.utc)
         old_proposal = self.factory.makeBranchMergeProposal(
             target_branch=target_branch,
             date_created=the_past,
@@ -1439,7 +1438,7 @@ class TestReclaimBranchSpaceJob(TestCaseWithFactory):
             self.factory.getUniqueInteger()
         )
         self.assertEqual(
-            datetime.timedelta(days=7),
+            timedelta(days=7),
             job.job.scheduled_start - job.job.date_created,
         )
 
diff --git a/lib/lp/code/model/tests/test_branchmergeproposal.py b/lib/lp/code/model/tests/test_branchmergeproposal.py
index eaec3f0..2810d3a 100644
--- a/lib/lp/code/model/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/model/tests/test_branchmergeproposal.py
@@ -4,7 +4,7 @@
 """Tests for BranchMergeProposals."""
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from difflib import unified_diff
 from unittest import TestCase
 
@@ -12,7 +12,6 @@ import transaction
 from fixtures import FakeLogger
 from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.restfulclient.errors import BadRequest
-from pytz import UTC
 from storm.locals import Store
 from testscenarios import WithScenarios, load_tests_apply_scenarios
 from testtools.matchers import (
@@ -569,7 +568,7 @@ class TestBranchMergeProposalRequestReview(TestCaseWithFactory):
         return source_branch.addLandingTarget(
             source_branch.owner,
             target_branch,
-            date_created=datetime(2000, 1, 1, 12, tzinfo=UTC),
+            date_created=datetime(2000, 1, 1, 12, tzinfo=timezone.utc),
             needs_review=needs_review,
         )
 
@@ -2618,7 +2617,7 @@ class TestScheduleDiffUpdates(TestCaseWithFactory):
         self.assertIsInstance(job, UpdatePreviewDiffJob)
 
     def test_getLatestDiffUpdateJob(self):
-        complete_date = datetime.now(UTC)
+        complete_date = datetime.now(timezone.utc)
 
         bmp = self.factory.makeBranchMergeProposal()
         failed_job = removeSecurityProxy(bmp.getLatestDiffUpdateJob())
@@ -2631,7 +2630,7 @@ class TestScheduleDiffUpdates(TestCaseWithFactory):
         self.assertEqual(failed_job.job_id, result.job_id)
 
     def test_ggetLatestDiffUpdateJob_correct_branch(self):
-        complete_date = datetime.now(UTC)
+        complete_date = datetime.now(timezone.utc)
 
         main_bmp = self.factory.makeBranchMergeProposal()
         second_bmp = self.factory.makeBranchMergeProposal()
@@ -2738,7 +2737,7 @@ class TestGetRevisionsSinceReviewStart(TestCaseWithFactory):
     def test_getRevisionsSinceReviewStart_groups(self):
         # Revisions that were scanned at the same time have the same
         # date_created.  These revisions are grouped together.
-        review_date = datetime(2009, 9, 10, tzinfo=UTC)
+        review_date = datetime(2009, 9, 10, tzinfo=timezone.utc)
         bmp = self.factory.makeBranchMergeProposal(date_created=review_date)
         with person_logged_in(bmp.registrant):
             bmp.requestReview(review_date)
diff --git a/lib/lp/code/model/tests/test_branchmergeproposaljobs.py b/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
index 872eb9f..9fd3e50 100644
--- a/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
+++ b/lib/lp/code/model/tests/test_branchmergeproposaljobs.py
@@ -4,9 +4,8 @@
 """Tests for branch merge proposal jobs."""
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from fixtures import FakeLogger
 from lazr.lifecycle.event import ObjectModifiedEvent
@@ -374,7 +373,7 @@ class TestUpdatePreviewDiffJob(DiffTestCase):
         bmp = self.createExampleBzrMerge()[0]
         job = UpdatePreviewDiffJob.create(bmp)
         job.acquireLease()
-        expiry_delta = job.lease_expires - datetime.now(pytz.UTC)
+        expiry_delta = job.lease_expires - datetime.now(timezone.utc)
         self.assertTrue(500 <= expiry_delta.seconds, expiry_delta)
 
     def assertCorrectPreviewDiffDelivery(self, bmp, delivery, logger):
@@ -489,7 +488,7 @@ class TestGenerateIncrementalDiffJob(DiffTestCase):
         job = GenerateIncrementalDiffJob.create(bmp, "old", "new")
         with dbuser("merge-proposal-jobs"):
             job.acquireLease()
-        expiry_delta = job.lease_expires - datetime.now(pytz.UTC)
+        expiry_delta = job.lease_expires - datetime.now(timezone.utc)
         self.assertTrue(500 <= expiry_delta.seconds, expiry_delta)
 
 
@@ -532,7 +531,7 @@ class TestBranchMergeProposalJobSource(TestCaseWithFactory):
             ),
         )
         minutes = config.codehosting.update_preview_diff_ready_timeout + 1
-        a_while_ago = datetime.now(pytz.UTC) - timedelta(minutes=minutes)
+        a_while_ago = datetime.now(timezone.utc) - timedelta(minutes=minutes)
         bmp_jobs.set(date_created=a_while_ago)
         [job] = self.job_source.iterReady()
         self.assertEqual(job.branch_merge_proposal, bmp)
diff --git a/lib/lp/code/model/tests/test_branchpuller.py b/lib/lp/code/model/tests/test_branchpuller.py
index 848081b..40e855a 100644
--- a/lib/lp/code/model/tests/test_branchpuller.py
+++ b/lib/lp/code/model/tests/test_branchpuller.py
@@ -3,9 +3,8 @@
 
 """Tests for the branch puller model code."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -45,7 +44,7 @@ class TestMirroringForImportedBranches(TestCaseWithFactory):
 
     def getNow(self):
         """Return a datetime representing 'now' in UTC."""
-        return datetime.now(pytz.timezone("UTC"))
+        return datetime.now(timezone.utc)
 
     def makeAnyBranch(self):
         return self.factory.makeAnyBranch(branch_type=self.branch_type)
@@ -62,7 +61,7 @@ class TestMirroringForImportedBranches(TestCaseWithFactory):
         # requestMirror() doesn't move the branch backwards in the queue of
         # branches that need mirroring.
         branch = self.makeAnyBranch()
-        past_time = datetime.now(pytz.UTC) - timedelta(days=1)
+        past_time = datetime.now(timezone.utc) - timedelta(days=1)
         removeSecurityProxy(branch).next_mirror_time = past_time
         branch.requestMirror()
         self.assertEqual(branch.next_mirror_time, past_time)
@@ -71,7 +70,7 @@ class TestMirroringForImportedBranches(TestCaseWithFactory):
         # requestMirror() sets the mirror request time to 'now' if
         # next_mirror_time is set and in the future.
         branch = self.makeAnyBranch()
-        future_time = datetime.now(pytz.UTC) + timedelta(days=1)
+        future_time = datetime.now(timezone.utc) + timedelta(days=1)
         removeSecurityProxy(branch).next_mirror_time = future_time
         branch.requestMirror()
         self.assertSqlAttributeEqualsDate(branch, "next_mirror_time", UTC_NOW)
diff --git a/lib/lp/code/model/tests/test_branchset.py b/lib/lp/code/model/tests/test_branchset.py
index 36b82e7..a6e2cd8 100644
--- a/lib/lp/code/model/tests/test_branchset.py
+++ b/lib/lp/code/model/tests/test_branchset.py
@@ -3,9 +3,8 @@
 
 """Tests for BranchSet."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from testtools.matchers import LessThan
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -67,11 +66,11 @@ class TestBranchSet(TestCaseWithFactory):
             removeSecurityProxy(branch).unscan(rescan=False)
         branches = [self.factory.makeProductBranch() for _ in range(5)]
         modified_dates = [
-            datetime(2010, 1, 1, tzinfo=pytz.UTC),
-            datetime(2015, 1, 1, tzinfo=pytz.UTC),
-            datetime(2014, 1, 1, tzinfo=pytz.UTC),
-            datetime(2020, 1, 1, tzinfo=pytz.UTC),
-            datetime(2019, 1, 1, tzinfo=pytz.UTC),
+            datetime(2010, 1, 1, tzinfo=timezone.utc),
+            datetime(2015, 1, 1, tzinfo=timezone.utc),
+            datetime(2014, 1, 1, tzinfo=timezone.utc),
+            datetime(2020, 1, 1, tzinfo=timezone.utc),
+            datetime(2019, 1, 1, tzinfo=timezone.utc),
         ]
         for branch, modified_date in zip(branches, modified_dates):
             self.factory.makeRevisionsForBranch(branch)
@@ -94,7 +93,9 @@ class TestBranchSet(TestCaseWithFactory):
                 getUtility(IBranchSet).getBranches(
                     branches[0].owner,
                     order_by=BranchListingSort.MOST_RECENTLY_CHANGED_FIRST,
-                    modified_since_date=datetime(2014, 12, 1, tzinfo=pytz.UTC),
+                    modified_since_date=datetime(
+                        2014, 12, 1, tzinfo=timezone.utc
+                    ),
                 )
             ),
         )
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index d5b4405..722ecb2 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -4,12 +4,11 @@
 """Test CI builds."""
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from textwrap import dedent
 from unittest.mock import Mock
 from urllib.request import urlopen
 
-import pytz
 from fixtures import MockPatchObject
 from pymacaroons import Macaroon
 from storm.locals import Store
@@ -228,7 +227,7 @@ class TestCIBuild(TestCaseWithFactory):
     def test_retry_resets_state(self):
         # Retrying a build resets most of the state attributes, but does
         # not modify the first dispatch time.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         build = self.factory.makeCIBuild()
         build.updateStatus(BuildStatus.BUILDING, date_started=now)
         build.updateStatus(BuildStatus.FAILEDTOBUILD)
@@ -538,7 +537,7 @@ class TestCIBuild(TestCaseWithFactory):
             git_repository=git_repository,
             commit_sha1="a39b604dcf9124d61cf94a1f9fffab638ee9a0cd",
             distro_arch_series=distroarchseries,
-            date_created=datetime(2014, 4, 25, 10, 38, 0, tzinfo=pytz.UTC),
+            date_created=datetime(2014, 4, 25, 10, 38, 0, tzinfo=timezone.utc),
             status=BuildStatus.FAILEDTOBUILD,
             builder=self.factory.makeBuilder(name="bob"),
             duration=timedelta(minutes=10),
diff --git a/lib/lp/code/model/tests/test_codeimport.py b/lib/lp/code/model/tests/test_codeimport.py
index 2541e9b..b38101b 100644
--- a/lib/lp/code/model/tests/test_codeimport.py
+++ b/lib/lp/code/model/tests/test_codeimport.py
@@ -4,10 +4,9 @@
 """Unit tests for methods of CodeImport and CodeImportSet."""
 
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from functools import partial
 
-import pytz
 from storm.store import Store
 from testscenarios import WithScenarios, load_tests_apply_scenarios
 from zope.component import getUtility
@@ -575,7 +574,7 @@ class TestCodeImportResultsAttribute(TestCodeImportBase):
         # The results query will order the results by job started time, with
         # the most recent import first.
         when = time_counter(
-            origin=datetime(2007, 9, 9, 12, tzinfo=pytz.UTC),
+            origin=datetime(2007, 9, 9, 12, tzinfo=timezone.utc),
             delta=timedelta(days=1),
         )
         first = self.factory.makeCodeImportResult(
@@ -599,7 +598,7 @@ class TestCodeImportResultsAttribute(TestCodeImportBase):
         # order (this wouldn't really happen) but it shows that the id of the
         # import result isn't used to sort by.
         when = time_counter(
-            origin=datetime(2007, 9, 11, 12, tzinfo=pytz.UTC),
+            origin=datetime(2007, 9, 11, 12, tzinfo=timezone.utc),
             delta=timedelta(days=-1),
         )
         first = self.factory.makeCodeImportResult(
diff --git a/lib/lp/code/model/tests/test_codeimportjob.py b/lib/lp/code/model/tests/test_codeimportjob.py
index 87f70f9..8cb025c 100644
--- a/lib/lp/code/model/tests/test_codeimportjob.py
+++ b/lib/lp/code/model/tests/test_codeimportjob.py
@@ -8,11 +8,10 @@ __all__ = [
 ]
 
 import io
-from datetime import datetime
+from datetime import datetime, timezone
 
 import transaction
 from pymacaroons import Macaroon
-from pytz import UTC
 from testtools.matchers import Equals, MatchesListwise, MatchesStructure
 from zope.component import getUtility
 from zope.publisher.xmlrpc import TestRequest
@@ -656,8 +655,10 @@ class TestCodeImportJobWorkflowNewJob(TestCaseWithFactory, AssertFailureMixin):
             code_import=code_import,
             machine=machine,
             status=FAILURE,
-            date_job_started=datetime(2000, 1, 1, 12, 0, 0, tzinfo=UTC),
-            date_created=datetime(2000, 1, 1, 12, 5, 0, tzinfo=UTC),
+            date_job_started=datetime(
+                2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc
+            ),
+            date_created=datetime(2000, 1, 1, 12, 5, 0, tzinfo=timezone.utc),
         )
         # Create a CodeImportResult that started a shorter time ago than the
         # effective update interval of the code import. This is the most
@@ -699,8 +700,10 @@ class TestCodeImportJobWorkflowNewJob(TestCaseWithFactory, AssertFailureMixin):
             code_import=code_import,
             machine=machine,
             status=FAILURE,
-            date_job_started=datetime(2000, 1, 1, 12, 0, 0, tzinfo=UTC),
-            date_created=datetime(2000, 1, 1, 12, 5, 0, tzinfo=UTC),
+            date_job_started=datetime(
+                2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc
+            ),
+            date_created=datetime(2000, 1, 1, 12, 5, 0, tzinfo=timezone.utc),
         )
         # When we create the job, its date due must be set to UTC_NOW.
         self.assertSqlAttributeEqualsDate(
@@ -831,7 +834,7 @@ class TestCodeImportJobWorkflowRequestJob(
         # Set date_due in the future. ICodeImportJob does not allow setting
         # date_due, so we must use removeSecurityProxy.
         removeSecurityProxy(pending_job).date_due = datetime(
-            2100, 1, 1, tzinfo=UTC
+            2100, 1, 1, tzinfo=timezone.utc
         )
         # requestJob sets both requesting_user and date_due.
         new_events = NewEvents()
@@ -855,7 +858,7 @@ class TestCodeImportJobWorkflowRequestJob(
         person = self.factory.makePerson()
         # Set date_due in the past. ICodeImportJob does not allow setting
         # date_due, so we must use removeSecurityProxy.
-        past_date = datetime(1900, 1, 1, tzinfo=UTC)
+        past_date = datetime(1900, 1, 1, tzinfo=timezone.utc)
         removeSecurityProxy(pending_job).date_due = past_date
         # requestJob only sets requesting_user.
         new_events = NewEvents()
@@ -1175,7 +1178,7 @@ class TestCodeImportJobWorkflowFinishJob(
         self.assertFinishJobPassesThroughJobField(
             "date_started",
             "date_job_started",
-            datetime(2008, 1, 1, tzinfo=UTC),
+            datetime(2008, 1, 1, tzinfo=timezone.utc),
         )
         unchecked_result_fields.remove("date_job_started")
 
diff --git a/lib/lp/code/model/tests/test_codeimportresult.py b/lib/lp/code/model/tests/test_codeimportresult.py
index 2ca40c5..deb7c51 100644
--- a/lib/lp/code/model/tests/test_codeimportresult.py
+++ b/lib/lp/code/model/tests/test_codeimportresult.py
@@ -3,9 +3,8 @@
 
 """Tests for CodeImportResult."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import UTC
 from testtools.matchers import LessThan
 
 from lp.code.interfaces.codeimportresult import ICodeImportResult
@@ -27,7 +26,7 @@ class TestCodeImportResult(TestCaseWithFactory):
         # commit, so it won't be the same as UTC_NOW.
         self.assertThat(
             result.date_created,
-            LessThan(datetime.utcnow().replace(tzinfo=UTC)),
+            LessThan(datetime.utcnow().replace(tzinfo=timezone.utc)),
         )
         self.assertEqual(result.date_created, result.date_job_finished)
 
diff --git a/lib/lp/code/model/tests/test_codereviewinlinecomment.py b/lib/lp/code/model/tests/test_codereviewinlinecomment.py
index eecfe83..66cb734 100644
--- a/lib/lp/code/model/tests/test_codereviewinlinecomment.py
+++ b/lib/lp/code/model/tests/test_codereviewinlinecomment.py
@@ -3,9 +3,8 @@
 
 """Tests for CodeReviewInlineComment{,Draft,Set}"""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import UTC
 from zope.component import getUtility
 
 from lp.code.interfaces.codereviewinlinecomment import (
@@ -148,7 +147,7 @@ class TestCodeReviewInlineComment(TestCaseWithFactory):
             previewdiff=previewdiff, person=person, comment=comment
         )
         old_comment = self.factory.makeCodeReviewComment(
-            date_created=datetime(2001, 1, 1, 12, tzinfo=UTC)
+            date_created=datetime(2001, 1, 1, 12, tzinfo=timezone.utc)
         )
         self.makeCodeReviewInlineComment(
             previewdiff=previewdiff,
diff --git a/lib/lp/code/model/tests/test_gitcollection.py b/lib/lp/code/model/tests/test_gitcollection.py
index df5c61b..7a67a2c 100644
--- a/lib/lp/code/model/tests/test_gitcollection.py
+++ b/lib/lp/code/model/tests/test_gitcollection.py
@@ -3,10 +3,9 @@
 
 """Tests for Git repository collections."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-import pytz
 from storm.expr import Asc, Desc
 from storm.store import EmptyResultSet, Store
 from testtools.matchers import Equals
@@ -241,14 +240,14 @@ class TestGitCollectionFilters(TestCaseWithFactory):
         # returned.
         old_repository = self.factory.makeGitRepository()
         removeSecurityProxy(old_repository).date_last_modified = datetime(
-            2008, 1, 1, tzinfo=pytz.UTC
+            2008, 1, 1, tzinfo=timezone.utc
         )
         new_repository = self.factory.makeGitRepository()
         removeSecurityProxy(new_repository).date_last_modified = datetime(
-            2009, 1, 1, tzinfo=pytz.UTC
+            2009, 1, 1, tzinfo=timezone.utc
         )
         repositories = self.all_repositories.modifiedSince(
-            datetime(2008, 6, 1, tzinfo=pytz.UTC)
+            datetime(2008, 6, 1, tzinfo=timezone.utc)
         )
         self.assertEqual(
             [new_repository], list(repositories.getRepositories())
@@ -580,7 +579,7 @@ class TestBranchMergeProposals(TestCaseWithFactory):
         bmp2 = self.factory.makeBranchMergeProposalForGit(
             target_ref=target, source_ref=ref2
         )
-        old_date = datetime.now(pytz.UTC) - timedelta(hours=1)
+        old_date = datetime.now(timezone.utc) - timedelta(hours=1)
         self.factory.makePreviewDiff(
             merge_proposal=bmp1, date_created=old_date
         )
diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py
index 81ab40f..28f4a5f 100644
--- a/lib/lp/code/model/tests/test_gitjob.py
+++ b/lib/lp/code/model/tests/test_gitjob.py
@@ -4,10 +4,9 @@
 """Tests for `GitJob`s."""
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from unittest import mock
 
-import pytz
 import transaction
 from fixtures import FakeLogger
 from lazr.lifecycle.snapshot import Snapshot
@@ -146,7 +145,7 @@ class TestGitRefScanJob(TestCaseWithFactory):
         job = GitRefScanJob.create(repository)
         paths = ("refs/heads/master", "refs/tags/1.0")
         author = repository.owner
-        author_date_start = datetime(2015, 1, 1, tzinfo=pytz.UTC)
+        author_date_start = datetime(2015, 1, 1, tzinfo=timezone.utc)
         author_date_gen = time_counter(author_date_start, timedelta(days=1))
         hosting_fixture = self.useFixture(
             GitHostingFixture(refs=self.makeFakeRefs(paths))
diff --git a/lib/lp/code/model/tests/test_gitref.py b/lib/lp/code/model/tests/test_gitref.py
index 2afc0ad..acf4821 100644
--- a/lib/lp/code/model/tests/test_gitref.py
+++ b/lib/lp/code/model/tests/test_gitref.py
@@ -5,9 +5,8 @@
 
 import hashlib
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import responses
 import transaction
 from breezy import urlutils
@@ -177,8 +176,8 @@ class TestGitRefGetCommits(TestCaseWithFactory):
                 author.preferredemail.email for author in self.authors
             ]
         self.dates = [
-            datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC),
-            datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC),
+            datetime(2015, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
+            datetime(2015, 1, 2, 0, 0, 0, tzinfo=timezone.utc),
         ]
         self.sha1_tip = hashlib.sha1(b"tip").hexdigest()
         self.sha1_root = hashlib.sha1(b"root").hexdigest()
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 2b55249..216d302 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -6,11 +6,10 @@
 import email
 import hashlib
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from functools import partial
 from textwrap import dedent
 
-import pytz
 import transaction
 from breezy import urlutils
 from fixtures import MockPatch
@@ -208,8 +207,8 @@ class TestParseGitCommits(TestCaseWithFactory):
         author = self.factory.makePerson()
         with person_logged_in(author):
             author_email = author.preferredemail.email
-        author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
-        committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)
+        author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
+        committer_date = datetime(2015, 1, 2, tzinfo=timezone.utc)
         commits = [
             {
                 "sha1": master_sha1,
@@ -257,7 +256,7 @@ class TestParseGitCommits(TestCaseWithFactory):
 
     def test_invalid_author_address(self):
         master_sha1 = hashlib.sha1(b"refs/heads/master").hexdigest()
-        author_date = datetime(2022, 1, 1, tzinfo=pytz.UTC)
+        author_date = datetime(2022, 1, 1, tzinfo=timezone.utc)
         commits = [
             {
                 "sha1": master_sha1,
@@ -298,7 +297,7 @@ class TestParseGitCommits(TestCaseWithFactory):
 
     def test_invalid_committer_address(self):
         master_sha1 = hashlib.sha1(b"refs/heads/master").hexdigest()
-        author_date = datetime(2022, 1, 1, tzinfo=pytz.UTC)
+        author_date = datetime(2022, 1, 1, tzinfo=timezone.utc)
         commits = [
             {
                 "sha1": master_sha1,
@@ -1404,7 +1403,7 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
         _, token = self.factory.makeAccessToken(target=self.repository)
         _, expired_token = self.factory.makeAccessToken(
             target=self.repository,
-            date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
+            date_expires=datetime.now(timezone.utc) - timedelta(minutes=1),
         )
         other_repository = self.factory.makeGitRepository()
         _, other_token = self.factory.makeAccessToken(target=other_repository)
@@ -1766,7 +1765,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
         # When a GitRepository receives an object modified event, the last
         # modified date is set to UTC_NOW.
         repository = self.factory.makeGitRepository(
-            date_created=datetime(2015, 2, 4, 17, 42, 0, tzinfo=pytz.UTC)
+            date_created=datetime(2015, 2, 4, 17, 42, 0, tzinfo=timezone.utc)
         )
         with notify_modified(
             removeSecurityProxy(repository), ["name"], user=repository.owner
@@ -1779,7 +1778,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
     def test_create_ref_sets_date_last_modified(self):
         self.useFixture(GitHostingFixture())
         repository = self.factory.makeGitRepository(
-            date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2015, 6, 1, tzinfo=timezone.utc)
         )
         [ref] = self.factory.makeGitRefs(repository=repository)
         new_refs_info = {
@@ -1796,7 +1795,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
     def test_update_ref_sets_date_last_modified(self):
         self.useFixture(GitHostingFixture())
         repository = self.factory.makeGitRepository(
-            date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2015, 6, 1, tzinfo=timezone.utc)
         )
         [ref] = self.factory.makeGitRefs(repository=repository)
         new_refs_info = {
@@ -1812,7 +1811,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
 
     def test_remove_ref_sets_date_last_modified(self):
         repository = self.factory.makeGitRepository(
-            date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC)
+            date_created=datetime(2015, 6, 1, tzinfo=timezone.utc)
         )
         [ref] = self.factory.makeGitRefs(repository=repository)
         repository.removeRefs({ref.path})
@@ -2439,7 +2438,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
         )
         naked_master.author_id = naked_master.committer_id = author.id
         naked_master.author_date = naked_master.committer_date = datetime.now(
-            pytz.UTC
+            timezone.utc
         )
         naked_master.commit_message = "message"
         self.useFixture(
@@ -2495,8 +2494,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
         author = self.factory.makePerson()
         with person_logged_in(author):
             author_email = author.preferredemail.email
-        author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
-        committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)
+        author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
+        committer_date = datetime(2015, 1, 2, tzinfo=timezone.utc)
         hosting_fixture = self.useFixture(
             GitHostingFixture(
                 commits=[
@@ -2587,7 +2586,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
         author = self.factory.makePerson()
         with person_logged_in(author):
             author_email = author.preferredemail.email
-        author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
+        author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
         hosting_fixture = self.useFixture(
             GitHostingFixture(
                 commits=[
@@ -3341,7 +3340,7 @@ class TestGitRepositoryRescan(TestCaseWithFactory):
         self.assertEqual(repository, job.repository)
 
     def test_getLatestScanJob(self):
-        complete_date = datetime.now(pytz.UTC)
+        complete_date = datetime.now(timezone.utc)
 
         repository = self.factory.makeGitRepository()
         failed_job = GitRefScanJob.create(repository)
@@ -3359,7 +3358,7 @@ class TestGitRepositoryRescan(TestCaseWithFactory):
         self.assertIsNone(result)
 
     def test_getLatestScanJob_correct_branch(self):
-        complete_date = datetime.now(pytz.UTC)
+        complete_date = datetime.now(timezone.utc)
 
         main_repository = self.factory.makeGitRepository()
         second_repository = self.factory.makeGitRepository()
@@ -4874,11 +4873,11 @@ class TestGitRepositorySet(TestCaseWithFactory):
         # We can get a collection of all repositories with a given sort order.
         repositories = [self.factory.makeGitRepository() for _ in range(5)]
         modified_dates = [
-            datetime(2010, 1, 1, tzinfo=pytz.UTC),
-            datetime(2015, 1, 1, tzinfo=pytz.UTC),
-            datetime(2014, 1, 1, tzinfo=pytz.UTC),
-            datetime(2020, 1, 1, tzinfo=pytz.UTC),
-            datetime(2019, 1, 1, tzinfo=pytz.UTC),
+            datetime(2010, 1, 1, tzinfo=timezone.utc),
+            datetime(2015, 1, 1, tzinfo=timezone.utc),
+            datetime(2014, 1, 1, tzinfo=timezone.utc),
+            datetime(2020, 1, 1, tzinfo=timezone.utc),
+            datetime(2019, 1, 1, tzinfo=timezone.utc),
         ]
         for repository, modified_date in zip(repositories, modified_dates):
             removeSecurityProxy(repository).date_last_modified = modified_date
@@ -4906,7 +4905,9 @@ class TestGitRepositorySet(TestCaseWithFactory):
                 self.repository_set.getRepositories(
                     repositories[0].owner,
                     order_by=GitListingSort.MOST_RECENTLY_CHANGED_FIRST,
-                    modified_since_date=datetime(2014, 12, 1, tzinfo=pytz.UTC),
+                    modified_since_date=datetime(
+                        2014, 12, 1, tzinfo=timezone.utc
+                    ),
                 )
             ),
         )
@@ -6596,7 +6597,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             permission=OAuthPermission.WRITE_PUBLIC,
             default_api_version="devel",
         )
-        date_expires = datetime.now(pytz.UTC) + timedelta(days=30)
+        date_expires = datetime.now(timezone.utc) + timedelta(days=30)
         response = webservice.named_post(
             repository_url,
             "issueAccessToken",
diff --git a/lib/lp/code/model/tests/test_revision.py b/lib/lp/code/model/tests/test_revision.py
index 150fa49..02a5aa1 100644
--- a/lib/lp/code/model/tests/test_revision.py
+++ b/lib/lp/code/model/tests/test_revision.py
@@ -4,11 +4,10 @@
 """Tests for Revisions."""
 
 import time
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from unittest import TestCase
 
 import psycopg2
-import pytz
 from storm.store import Store
 from testtools.matchers import Equals
 from zope.component import getUtility
@@ -42,7 +41,7 @@ class TestRevisionCreationDate(TestCaseWithFactory):
 
     def test_new_past_revision_date(self):
         # A revision created with a revision date in the past works fine.
-        past_date = datetime(2009, 1, 1, tzinfo=pytz.UTC)
+        past_date = datetime(2009, 1, 1, tzinfo=timezone.utc)
         revision = RevisionSet().new(
             "rev_id", "log body", past_date, "author", [], {}
         )
@@ -51,7 +50,7 @@ class TestRevisionCreationDate(TestCaseWithFactory):
     def test_new_future_revision_date(self):
         # A revision with a future date gets the revision date set to
         # date_created.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         future_date = now + timedelta(days=1)
         revision = RevisionSet().new(
             "rev_id", "log body", future_date, "author", [], {}
@@ -101,7 +100,7 @@ class TestRevisionKarma(TestCaseWithFactory):
         author = self.factory.makePerson()
         rev = self.factory.makeRevision(
             author=author.preferredemail.email,
-            revision_date=datetime.now(pytz.UTC) - timedelta(days=5),
+            revision_date=datetime.now(timezone.utc) - timedelta(days=5),
         )
         branch = self.factory.makeProductBranch()
         branch.createBranchRevision(1, rev)
@@ -156,7 +155,7 @@ class TestRevisionKarma(TestCaseWithFactory):
         author = self.factory.makePerson()
         rev = self.factory.makeRevision(
             author=author,
-            revision_date=datetime.now(pytz.UTC) + timedelta(days=5),
+            revision_date=datetime.now(timezone.utc) + timedelta(days=5),
         )
         branch = self.factory.makeProductBranch()
         karma = rev.allocateKarma(branch)
@@ -244,7 +243,8 @@ class TestRevisionSet(TestCaseWithFactory):
         )
         self.assertEqual(bzr_revisions[0].message, rev_1.log_body)
         self.assertEqual(
-            datetime(1970, 1, 1, 0, 0, tzinfo=pytz.UTC), rev_1.revision_date
+            datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc),
+            rev_1.revision_date,
         )
         self.assertEqual([], rev_1.parents)
         # Revision properties starting with 'deb-pristine-delta' aren't
@@ -371,7 +371,7 @@ class GetPublicRevisionsTestCase(TestCaseWithFactory):
         # Since the tests order by date, but also limit to the last 30
         # days, we want a time counter that starts 10 days ago.
         self.date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=10),
+            datetime.now(timezone.utc) - timedelta(days=10),
             delta=timedelta(days=1),
         )
 
@@ -452,7 +452,7 @@ class RevisionTestMixin:
     def testRevisionDateRange(self):
         # Revisions where the revision_date is older than the day_limit are
         # not returned.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         day_limit = 5
         # Make the first revision earlier than our day limit.
         rev1 = self._makeRevision(
@@ -612,7 +612,7 @@ class TestGetRecentRevisionsForProduct(GetPublicRevisionsTestCase):
     def testRevisionDateRange(self):
         # Revisions where the revision_date is older than the day_limit are
         # not returned.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         day_limit = 5
         # Make the first revision earlier than our day limit.
         rev1 = self._makeRevision(
@@ -694,18 +694,18 @@ class TestTipRevisionsForBranches(TestCase):
         # timestampToDatetime should convert a negative, fractional timestamp
         # into a valid, sane datetime object.
         revision_set = removeSecurityProxy(getUtility(IRevisionSet))
-        UTC = pytz.timezone("UTC")
         timestamp = -0.5
         date = revision_set._timestampToDatetime(timestamp)
-        self.assertEqual(date, datetime(1969, 12, 31, 23, 59, 59, 500000, UTC))
+        self.assertEqual(
+            date, datetime(1969, 12, 31, 23, 59, 59, 500000, timezone.utc)
+        )
 
     def test_timestampToDatetime(self):
         # timestampTODatetime should convert a regular timestamp into a valid,
         # sane datetime object.
         revision_set = removeSecurityProxy(getUtility(IRevisionSet))
-        UTC = pytz.timezone("UTC")
         timestamp = time.time()
-        date = datetime.fromtimestamp(timestamp, tz=UTC)
+        date = datetime.fromtimestamp(timestamp, tz=timezone.utc)
         self.assertEqual(date, revision_set._timestampToDatetime(timestamp))
 
 
@@ -797,7 +797,7 @@ class TestUpdateRevisionCacheForBranch(RevisionCacheTestCase):
         # Revisions older than the 30 day epoch are not added to the cache.
 
         # Start 33 days ago.
-        epoch = datetime.now(pytz.UTC) - timedelta(days=30)
+        epoch = datetime.now(timezone.utc) - timedelta(days=30)
         date_generator = time_counter(
             epoch - timedelta(days=3), delta=timedelta(days=2)
         )
@@ -818,7 +818,7 @@ class TestUpdateRevisionCacheForBranch(RevisionCacheTestCase):
         # If there are already revisions in the cache for the branch, updating
         # the branch again will only add the new revisions.
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=29),
+            datetime.now(timezone.utc) - timedelta(days=29),
             delta=timedelta(days=1),
         )
         # Initially add in 4 revisions.
@@ -982,7 +982,7 @@ class TestPruneRevisionCache(RevisionCacheTestCase):
     def test_old_revisions_removed(self):
         # Revisions older than 30 days are removed.
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=33),
+            datetime.now(timezone.utc) - timedelta(days=33),
             delta=timedelta(days=2),
         )
         for i in range(4):
@@ -997,7 +997,7 @@ class TestPruneRevisionCache(RevisionCacheTestCase):
     def test_pruning_limit(self):
         # The prune will only remove at most the parameter rows.
         date_generator = time_counter(
-            datetime.now(pytz.UTC) - timedelta(days=33),
+            datetime.now(timezone.utc) - timedelta(days=33),
             delta=timedelta(days=2),
         )
         for i in range(4):
diff --git a/lib/lp/code/model/tests/test_revisioncache.py b/lib/lp/code/model/tests/test_revisioncache.py
index aba5fce..df03eb9 100644
--- a/lib/lp/code/model/tests/test_revisioncache.py
+++ b/lib/lp/code/model/tests/test_revisioncache.py
@@ -3,9 +3,8 @@
 
 """Tests relating to the revision cache."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -98,7 +97,7 @@ class TestRevisionCache(TestCaseWithFactory):
     def test_revision_ordering(self):
         # Revisions are returned most recent first.
         tc = time_counter(
-            origin=datetime.now(pytz.UTC) - timedelta(days=15),
+            origin=datetime.now(timezone.utc) - timedelta(days=15),
             delta=timedelta(days=1),
         )
         # Make four cached revisions spanning 15, 14, 13 and 12 days ago.
@@ -132,7 +131,7 @@ class TestRevisionCache(TestCaseWithFactory):
         # Only revisions in the last 30 days are returned, even if the
         # revision cache table hasn't been trimmed lately.
         tc = time_counter(
-            origin=datetime.now(pytz.UTC) - timedelta(days=27),
+            origin=datetime.now(timezone.utc) - timedelta(days=27),
             delta=timedelta(days=-2),
         )
         # Make four cached revisions spanning 33, 31, 29, and 27 days ago.
diff --git a/lib/lp/code/model/tests/test_sourcepackagerecipe.py b/lib/lp/code/model/tests/test_sourcepackagerecipe.py
index 6f4d753..46cf603 100644
--- a/lib/lp/code/model/tests/test_sourcepackagerecipe.py
+++ b/lib/lp/code/model/tests/test_sourcepackagerecipe.py
@@ -4,12 +4,11 @@
 """Tests for the SourcePackageRecipe content type."""
 
 import textwrap
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import transaction
 from brzbuildrecipe.recipe import ForbiddenInstructionError
 from lazr.restfulclient.errors import BadRequest
-from pytz import UTC
 from storm.locals import Store
 from testtools.matchers import Equals
 from zope.component import getUtility
@@ -697,7 +696,7 @@ class TestSourcePackageRecipeMixin:
         pending_build.queueBuild()
         past_build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         past_build.queueBuild()
-        removeSecurityProxy(past_build).datebuilt = datetime.now(UTC)
+        removeSecurityProxy(past_build).datebuilt = datetime.now(timezone.utc)
         with person_logged_in(recipe.owner):
             recipe.destroySelf()
         # Show no database constraints were violated
@@ -781,7 +780,7 @@ class TestSourcePackageRecipeMixin:
                 distroseries=series,
                 pocket=PackagePublishingPocket.RELEASE,
                 date_created=(
-                    datetime.now(UTC) - timedelta(hours=24, seconds=1)
+                    datetime.now(timezone.utc) - timedelta(hours=24, seconds=1)
                 ),
             )
         stale_recipes = SourcePackageRecipe.findStaleDailyBuilds()
@@ -1381,7 +1380,7 @@ class RecipeDateLastModified(TestCaseWithFactory):
 
     def setUp(self):
         TestCaseWithFactory.setUp(self, "test@xxxxxxxxxxxxx")
-        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        date_created = datetime(2000, 1, 1, 12, tzinfo=timezone.utc)
         self.recipe = self.factory.makeSourcePackageRecipe(
             date_created=date_created
         )
diff --git a/lib/lp/code/model/tests/test_sourcepackagerecipebuild.py b/lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
index 39b6193..bd2ad6e 100644
--- a/lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
+++ b/lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
@@ -3,10 +3,9 @@
 
 """Tests for source package builds."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import transaction
-from pytz import utc
 from storm.locals import Store
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -307,7 +306,8 @@ class TestSourcePackageRecipeBuild(TestCaseWithFactory):
             requester=recipe.owner,
             distroseries=series,
             pocket=PackagePublishingPocket.RELEASE,
-            date_created=datetime.now(utc) - timedelta(hours=24, seconds=1),
+            date_created=datetime.now(timezone.utc)
+            - timedelta(hours=24, seconds=1),
         )
         removeSecurityProxy(recipe).is_stale = True
 
@@ -373,7 +373,8 @@ class TestSourcePackageRecipeBuild(TestCaseWithFactory):
             requester=recipe.owner,
             distroseries=list(recipe.distroseries)[0],
             pocket=PackagePublishingPocket.RELEASE,
-            date_created=datetime.now(utc) - timedelta(hours=24, seconds=1),
+            date_created=datetime.now(timezone.utc)
+            - timedelta(hours=24, seconds=1),
             status=BuildStatus.FULLYBUILT,
         )
         daily_builds = SourcePackageRecipeBuild.makeDailyBuilds()
@@ -394,7 +395,7 @@ class TestSourcePackageRecipeBuild(TestCaseWithFactory):
                 requester=recipe.owner,
                 distroseries=list(recipe.distroseries)[0],
                 pocket=PackagePublishingPocket.RELEASE,
-                date_created=datetime.now(utc) - timediff,
+                date_created=datetime.now(timezone.utc) - timediff,
                 status=BuildStatus.FULLYBUILT,
             )
         daily_builds = SourcePackageRecipeBuild.makeDailyBuilds()
@@ -413,7 +414,7 @@ class TestSourcePackageRecipeBuild(TestCaseWithFactory):
             requester=recipe.owner,
             distroseries=list(recipe.distroseries)[0],
             pocket=PackagePublishingPocket.RELEASE,
-            date_created=datetime.now(utc) - timedelta(hours=8),
+            date_created=datetime.now(timezone.utc) - timedelta(hours=8),
             status=BuildStatus.FULLYBUILT,
         )
         daily_builds = SourcePackageRecipeBuild.makeDailyBuilds()
diff --git a/lib/lp/code/scripts/tests/test_repack_git_repositories.py b/lib/lp/code/scripts/tests/test_repack_git_repositories.py
index a8370c1..4fc4f93 100644
--- a/lib/lp/code/scripts/tests/test_repack_git_repositories.py
+++ b/lib/lp/code/scripts/tests/test_repack_git_repositories.py
@@ -5,10 +5,9 @@
 
 import logging
 import threading
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from wsgiref.simple_server import WSGIRequestHandler, make_server
 
-import pytz
 import transaction
 from zope.security.proxy import removeSecurityProxy
 
@@ -261,7 +260,7 @@ class TestRequestGitRepack(TestCaseWithFactory):
         # If we pretend that the last repack request was long enough ago,
         # then a third run requests another repack.
         removeSecurityProxy(repo).date_last_repacked = datetime.now(
-            pytz.UTC
+            timezone.utc
         ) - timedelta(minutes=config.codehosting.auto_repack_frequency + 1)
         self.runScript_with_Turnip(expected_count=1)
 
diff --git a/lib/lp/code/stories/branches/xx-branch-index.rst b/lib/lp/code/stories/branches/xx-branch-index.rst
index c6be957..e2946b1 100644
--- a/lib/lp/code/stories/branches/xx-branch-index.rst
+++ b/lib/lp/code/stories/branches/xx-branch-index.rst
@@ -3,10 +3,9 @@ Branch Details
 
 Imports used later in the document:
 
-    >>> import pytz
     >>> from zope.component import getUtility
     >>> from lp.services.database.sqlbase import flush_database_updates
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> from lp.code.enums import BranchType
     >>> from lp.code.bzr import BranchFormat, RepositoryFormat
     >>> from lp.code.interfaces.branchlookup import IBranchLookup
@@ -239,7 +238,7 @@ it has been mirrored:
     ...     title="Disabled branch",
     ... )
     >>> branch.last_mirrored = datetime(
-    ...     year=2007, month=10, day=1, tzinfo=pytz.timezone("UTC")
+    ...     year=2007, month=10, day=1, tzinfo=timezone.utc
     ... )
     >>> branch.next_mirror_time = None
     >>> flush_database_updates()
diff --git a/lib/lp/code/stories/branches/xx-branch-mirror-failures.rst b/lib/lp/code/stories/branches/xx-branch-mirror-failures.rst
index df7d11e..98fe94c 100644
--- a/lib/lp/code/stories/branches/xx-branch-mirror-failures.rst
+++ b/lib/lp/code/stories/branches/xx-branch-mirror-failures.rst
@@ -65,12 +65,11 @@ A subsequent failure shows:
 If the mirror had been mirrored at some stage, the error is slightly
 different.
 
-    >>> import pytz
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> login("eric@xxxxxxxxxxx")  # To get Launchpad.Edit on the branch.
     >>> mirror_branch = getUtility(IBranchLookup).getByUniqueName(mirror_name)
     >>> mirror_branch.last_mirrored = datetime(
-    ...     2007, 12, 25, 12, tzinfo=pytz.UTC
+    ...     2007, 12, 25, 12, tzinfo=timezone.utc
     ... )
     >>> mirror_branch.startMirroring()
     >>> mirror_branch.mirrorFailed('Cannot access branch at "example.com".')
diff --git a/lib/lp/code/stories/branches/xx-branch-tag-cloud.rst b/lib/lp/code/stories/branches/xx-branch-tag-cloud.rst
index 0297e28..7a63908 100644
--- a/lib/lp/code/stories/branches/xx-branch-tag-cloud.rst
+++ b/lib/lp/code/stories/branches/xx-branch-tag-cloud.rst
@@ -6,9 +6,8 @@ available bazaar branches is shown to the user.
 
     >>> login(ANONYMOUS)
     >>> from lp.code.tests.helpers import make_project_cloud_data
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
-    >>> now = datetime.now(pytz.UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> make_project_cloud_data(
     ...     factory,
     ...     [
diff --git a/lib/lp/code/stories/branches/xx-code-review-comments.rst b/lib/lp/code/stories/branches/xx-code-review-comments.rst
index 930dac7..450d903 100644
--- a/lib/lp/code/stories/branches/xx-code-review-comments.rst
+++ b/lib/lp/code/stories/branches/xx-code-review-comments.rst
@@ -175,9 +175,8 @@ shown as part of the conversation at the time they were pushed to Launchpad.
     >>> login("admin@xxxxxxxxxxxxx")
     >>> from lp.code.tests.helpers import add_revision_to_branch
     >>> bmp = factory.makeBranchMergeProposal()
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
-    >>> review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> review_date = datetime(2009, 9, 10, tzinfo=timezone.utc)
     >>> bmp.requestReview(review_date)
     >>> revision_date = review_date + timedelta(days=1)
     >>> for date in range(2):
diff --git a/lib/lp/code/stories/branches/xx-personproduct-branch-listings.rst b/lib/lp/code/stories/branches/xx-personproduct-branch-listings.rst
index 813e97e..ebc2474 100644
--- a/lib/lp/code/stories/branches/xx-personproduct-branch-listings.rst
+++ b/lib/lp/code/stories/branches/xx-personproduct-branch-listings.rst
@@ -21,10 +21,9 @@ that only the fooix branches are shown.
 
     >>> login(ANONYMOUS)
     >>> from lp.testing import time_counter
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> date_generator = time_counter(
-    ...     datetime(2007, 12, 1, tzinfo=pytz.UTC), timedelta(days=1)
+    ...     datetime(2007, 12, 1, tzinfo=timezone.utc), timedelta(days=1)
     ... )
     >>> branch = factory.makeProductBranch(
     ...     owner=eric,
diff --git a/lib/lp/code/stories/codeimport/xx-admin-codeimport.rst b/lib/lp/code/stories/codeimport/xx-admin-codeimport.rst
index 77a516c..aabece2 100644
--- a/lib/lp/code/stories/codeimport/xx-admin-codeimport.rst
+++ b/lib/lp/code/stories/codeimport/xx-admin-codeimport.rst
@@ -290,9 +290,10 @@ Now set the job as running.
 Set the started time to 2h 20m ago, and the approximate datetime
 should show this as 2 hours.
 
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
-    >>> date_started = datetime.now(pytz.UTC) - timedelta(hours=2, minutes=20)
+    >>> from datetime import datetime, timedelta, timezone
+    >>> date_started = datetime.now(timezone.utc) - timedelta(
+    ...     hours=2, minutes=20
+    ... )
     >>> code_import = make_running_import(
     ...     code_import,
     ...     date_started=date_started,
@@ -322,7 +323,7 @@ last import completed, and so in this case in about three hours.
 
     >>> login("david.allouche@xxxxxxxxxxxxx")
     >>> from lp.code.tests.codeimporthelpers import make_finished_import
-    >>> date_finished = datetime(2007, 9, 10, 12, tzinfo=pytz.UTC)
+    >>> date_finished = datetime(2007, 9, 10, 12, tzinfo=timezone.utc)
     >>> code_import = get_import_for_branch_name(
     ...     svn_import_branch_unique_name
     ... )
diff --git a/lib/lp/code/stories/codeimport/xx-edit-codeimport.rst b/lib/lp/code/stories/codeimport/xx-edit-codeimport.rst
index 0e3e16e..11a3223 100644
--- a/lib/lp/code/stories/codeimport/xx-edit-codeimport.rst
+++ b/lib/lp/code/stories/codeimport/xx-edit-codeimport.rst
@@ -102,9 +102,8 @@ imports.
     ...     make_finished_import,
     ...     get_import_for_branch_name,
     ... )
-    >>> from datetime import datetime
-    >>> import pytz
-    >>> date_finished = datetime(2007, 9, 10, 12, tzinfo=pytz.UTC)
+    >>> from datetime import datetime, timezone
+    >>> date_finished = datetime(2007, 9, 10, 12, tzinfo=timezone.utc)
     >>> code_import = get_import_for_branch_name(
     ...     svn_import_branch_unique_name
     ... )
diff --git a/lib/lp/code/stories/feeds/xx-branch-atom.rst b/lib/lp/code/stories/feeds/xx-branch-atom.rst
index 426f4d7..a58003d 100644
--- a/lib/lp/code/stories/feeds/xx-branch-atom.rst
+++ b/lib/lp/code/stories/feeds/xx-branch-atom.rst
@@ -16,10 +16,9 @@ Create some specific branches to use for this test
 
     >>> login(ANONYMOUS)
     >>> from lp.testing import time_counter
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> date_generator = time_counter(
-    ...     datetime(2007, 12, 1, tzinfo=pytz.UTC), timedelta(days=1)
+    ...     datetime(2007, 12, 1, tzinfo=timezone.utc), timedelta(days=1)
     ... )
     >>> def make_branch(owner, product, name):
     ...     global factory, date_generator
diff --git a/lib/lp/code/stories/feeds/xx-revision-atom.rst b/lib/lp/code/stories/feeds/xx-revision-atom.rst
index 0b98e04..a11c667 100644
--- a/lib/lp/code/stories/feeds/xx-revision-atom.rst
+++ b/lib/lp/code/stories/feeds/xx-revision-atom.rst
@@ -13,13 +13,14 @@ Create some specific branches to use for this test
 
     >>> login(ANONYMOUS)
     >>> from lp.testing import time_counter
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
 
 Since the feed only shows revisions from the last 30 days, we need recent
 revisions.
 
-    >>> initial_revision_date = datetime.now(pytz.UTC) - timedelta(days=10)
+    >>> initial_revision_date = datetime.now(timezone.utc) - timedelta(
+    ...     days=10
+    ... )
     >>> date_generator = time_counter(
     ...     initial_revision_date, timedelta(days=1)
     ... )
diff --git a/lib/lp/code/stories/webservice/xx-branch.rst b/lib/lp/code/stories/webservice/xx-branch.rst
index 5d9e0f6..6d27ce6 100644
--- a/lib/lp/code/stories/webservice/xx-branch.rst
+++ b/lib/lp/code/stories/webservice/xx-branch.rst
@@ -7,8 +7,7 @@ branches. You can either interact with the branches themselves using
 information about the branches and how they relate to the rest of
 the things on Launchpad.
 
-    >>> from datetime import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
     >>> from zope.security.proxy import removeSecurityProxy
     >>> from lp.code.enums import BranchLifecycleStatus, BranchType
     >>> from lazr.restful.marshallers import DateTimeFieldMarshaller
@@ -55,9 +54,9 @@ At the moment, it's not scheduled to be mirrored.
 But we can ask for it to be mirrored using the webservice:
 
     >>> branch_url = "/" + branch.unique_name
-    >>> start_time = datetime.now(pytz.UTC)
+    >>> start_time = datetime.now(timezone.utc)
     >>> response = webservice.named_post(branch_url, "requestMirror")
-    >>> end_time = datetime.now(pytz.UTC)
+    >>> end_time = datetime.now(timezone.utc)
     >>> new_mirror_time = get_as_datetime(response)
     >>> branch.next_mirror_time == new_mirror_time
     True
@@ -86,7 +85,7 @@ time goes on.
     ...     product=fooix,
     ...     name="trunk",
     ...     title="The Fooix Trunk",
-    ...     date_created=datetime(2009, 1, 1, tzinfo=pytz.UTC),
+    ...     date_created=datetime(2009, 1, 1, tzinfo=timezone.utc),
     ... )
     >>> feature_branch = factory.makeAnyBranch(
     ...     owner=eric,
diff --git a/lib/lp/code/tests/branch_helper.py b/lib/lp/code/tests/branch_helper.py
index 2337916..417683b 100644
--- a/lib/lp/code/tests/branch_helper.py
+++ b/lib/lp/code/tests/branch_helper.py
@@ -7,16 +7,15 @@ __all__ = [
     "reset_all_branch_last_modified",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.component import getUtility
 
 from lp.code.interfaces.branchcollection import IAllBranches
 from lp.testing import celebrity_logged_in
 
 
-def reset_all_branch_last_modified(last_modified=datetime.now(pytz.UTC)):
+def reset_all_branch_last_modified(last_modified=datetime.now(timezone.utc)):
     """Reset the date_last_modifed value on all the branches.
 
     DO NOT use this in a non-pagetest.
diff --git a/lib/lp/code/tests/codeimporthelpers.py b/lib/lp/code/tests/codeimporthelpers.py
index db0797a..b4cafb0 100644
--- a/lib/lp/code/tests/codeimporthelpers.py
+++ b/lib/lp/code/tests/codeimporthelpers.py
@@ -10,9 +10,8 @@ __all__ = [
 ]
 
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import UTC
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -129,10 +128,11 @@ def make_finished_import(
 def make_all_result_types(code_import, factory, machine, start, count):
     """Make a code import result of each possible type for the code import."""
     start_dates = time_counter(
-        datetime(2007, 12, 1, 12, tzinfo=UTC), timedelta(days=1)
+        datetime(2007, 12, 1, 12, tzinfo=timezone.utc), timedelta(days=1)
     )
     end_dates = time_counter(
-        datetime(2007, 12, 1, 13, tzinfo=UTC), timedelta(days=1, hours=1)
+        datetime(2007, 12, 1, 13, tzinfo=timezone.utc),
+        timedelta(days=1, hours=1),
     )
     for result_status in sorted(CodeImportResultStatus.items)[
         start : start + count
diff --git a/lib/lp/code/tests/test_helpers.py b/lib/lp/code/tests/test_helpers.py
index 491a493..2b05f4f 100644
--- a/lib/lp/code/tests/test_helpers.py
+++ b/lib/lp/code/tests/test_helpers.py
@@ -3,9 +3,8 @@
 
 """Test the code test helpers found in helpers.py."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.component import getUtility
 
 from lp.code.interfaces.branchcollection import IAllBranches
@@ -22,7 +21,7 @@ class TestMakeProjectCloudData(TestCaseWithFactory):
 
     def test_single_project(self):
         # Make a single project with one commit from one person.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         commit_time = now - timedelta(days=2)
         make_project_cloud_data(
             self.factory,
diff --git a/lib/lp/code/tests/test_seriessourcepackagebranch.py b/lib/lp/code/tests/test_seriessourcepackagebranch.py
index 52ba230..8746307 100644
--- a/lib/lp/code/tests/test_seriessourcepackagebranch.py
+++ b/lib/lp/code/tests/test_seriessourcepackagebranch.py
@@ -3,9 +3,8 @@
 
 """Tests for ISeriesSourcePackageBranch."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from zope.component import getUtility
 
@@ -33,7 +32,7 @@ class TestSeriesSourcePackageBranch(TestCaseWithFactory):
         sourcepackagename = self.factory.makeSourcePackageName()
         registrant = self.factory.makePerson()
         branch = self.factory.makeAnyBranch()
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         sspb = SeriesSourcePackageBranchSet.new(
             distroseries,
             PackagePublishingPocket.RELEASE,
diff --git a/lib/lp/code/vocabularies/tests/test_gitref_vocabularies.py b/lib/lp/code/vocabularies/tests/test_gitref_vocabularies.py
index 07789ac..0ad6d9c 100644
--- a/lib/lp/code/vocabularies/tests/test_gitref_vocabularies.py
+++ b/lib/lp/code/vocabularies/tests/test_gitref_vocabularies.py
@@ -3,9 +3,8 @@
 
 """Test the Git reference vocabularies."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from testtools.matchers import MatchesStructure
 from zope.schema.vocabulary import SimpleTerm
 from zope.security.proxy import removeSecurityProxy
@@ -122,7 +121,7 @@ class TestGitRefVocabulary(TestCaseWithFactory):
         removeSecurityProxy(
             ref_master.repository
         )._default_branch = ref_master.path
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         removeSecurityProxy(ref_master_old).committer_date = now - timedelta(
             days=1
         )
@@ -240,7 +239,7 @@ class TestGitBranchVocabulary(TestCaseWithFactory):
         removeSecurityProxy(
             ref_master.repository
         )._default_branch = ref_master.path
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         removeSecurityProxy(ref_master_old).committer_date = now - timedelta(
             days=1
         )
diff --git a/lib/lp/code/xmlrpc/codehosting.py b/lib/lp/code/xmlrpc/codehosting.py
index 339647d..bd68389 100644
--- a/lib/lp/code/xmlrpc/codehosting.py
+++ b/lib/lp/code/xmlrpc/codehosting.py
@@ -10,9 +10,8 @@ __all__ = [
 ]
 
 
-import datetime
+from datetime import datetime, timezone
 
-import pytz
 import six
 import transaction
 from breezy.urlutils import escape, unescape
@@ -63,14 +62,12 @@ from lp.services.webapp.interaction import setupInteractionForPerson
 from lp.xmlrpc import faults
 from lp.xmlrpc.helpers import return_fault
 
-UTC = pytz.timezone("UTC")
-
 
 def datetime_from_tuple(time_tuple):
     """Create a datetime from a sequence that quacks like time.struct_time.
 
     The tm_isdst is (index 8) is ignored. The created datetime uses
-    tzinfo=UTC.
+    tzinfo=timezone.utc.
     """
     [
         year,
@@ -83,8 +80,8 @@ def datetime_from_tuple(time_tuple):
         unused,
         unused,
     ] = time_tuple
-    return datetime.datetime(
-        year, month, day, hour, minute, second, tzinfo=UTC
+    return datetime(
+        year, month, day, hour, minute, second, tzinfo=timezone.utc
     )
 
 
diff --git a/lib/lp/code/xmlrpc/tests/test_codehosting.py b/lib/lp/code/xmlrpc/tests/test_codehosting.py
index c604c36..2dd17a3 100644
--- a/lib/lp/code/xmlrpc/tests/test_codehosting.py
+++ b/lib/lp/code/xmlrpc/tests/test_codehosting.py
@@ -3,11 +3,10 @@
 
 """Tests for the internal codehosting API."""
 
-import datetime
 import os
 import threading
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from breezy import controldir
 from breezy.urlutils import escape
@@ -63,8 +62,6 @@ from lp.testing.layers import (
 )
 from lp.xmlrpc import faults
 
-UTC = pytz.timezone("UTC")
-
 
 def get_logged_in_username(requester=None):
     """Return the username of the logged in person.
@@ -285,8 +282,8 @@ class CodehostingTest(WithScenarios, TestCaseWithFactory):
 
     def test_recordSuccess(self):
         # recordSuccess must insert the given data into ScriptActivity.
-        started = datetime.datetime(2007, 7, 5, 19, 32, 1, tzinfo=UTC)
-        completed = datetime.datetime(2007, 7, 5, 19, 34, 24, tzinfo=UTC)
+        started = datetime(2007, 7, 5, 19, 32, 1, tzinfo=timezone.utc)
+        completed = datetime(2007, 7, 5, 19, 34, 24, tzinfo=timezone.utc)
         started_tuple = tuple(started.utctimetuple())
         completed_tuple = tuple(completed.utctimetuple())
         success = self.codehosting_api.recordSuccess(
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index b92f599..dab3c43 100644
--- a/lib/lp/code/xmlrpc/tests/test_git.py
+++ b/lib/lp/code/xmlrpc/tests/test_git.py
@@ -6,10 +6,9 @@
 import hashlib
 import uuid
 import xmlrpc.client
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.parse import quote
 
-import pytz
 import six
 from fixtures import FakeLogger
 from pymacaroons import Macaroon
@@ -3161,7 +3160,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
         self.assertEqual(repository, job.repository)
 
     def assertSetsRepackData(self, repo, auth_params):
-        start_time = datetime.now(pytz.UTC)
+        start_time = datetime.now(timezone.utc)
         self.assertIsNone(
             self.assertDoesNotFault(
                 None,
@@ -3171,7 +3170,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
                 auth_params,
             )
         )
-        end_time = datetime.now(pytz.UTC)
+        end_time = datetime.now(timezone.utc)
         naked_repo = removeSecurityProxy(repo)
         self.assertEqual(5, naked_repo.loose_object_count)
         self.assertEqual(2, naked_repo.pack_count)
@@ -3638,7 +3637,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
         requester = self.factory.makePerson()
         secret, _ = self.factory.makeAccessToken(
             owner=requester,
-            date_expires=datetime.now(pytz.UTC) - timedelta(days=1),
+            date_expires=datetime.now(timezone.utc) - timedelta(days=1),
         )
         self.assertFault(
             faults.Unauthorized,
@@ -4058,7 +4057,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
         )
 
     def assertUpdatesRepackStats(self, repo):
-        start_time = datetime.now(pytz.UTC)
+        start_time = datetime.now(timezone.utc)
         self.assertIsNone(
             self.assertDoesNotFault(
                 None,
@@ -4067,7 +4066,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
                 {"loose_object_count": 5, "pack_count": 2},
             )
         )
-        end_time = datetime.now(pytz.UTC)
+        end_time = datetime.now(timezone.utc)
         naked_repo = removeSecurityProxy(repo)
         self.assertEqual(5, naked_repo.loose_object_count)
         self.assertEqual(2, naked_repo.pack_count)
diff --git a/lib/lp/codehosting/puller/__init__.py b/lib/lp/codehosting/puller/__init__.py
index 646917e..9e1dfbd 100644
--- a/lib/lp/codehosting/puller/__init__.py
+++ b/lib/lp/codehosting/puller/__init__.py
@@ -4,9 +4,8 @@
 __all__ = ["get_lock_id_for_branch_id", "mirror"]
 
 
-import datetime
+from datetime import datetime, timezone
 
-import pytz
 from twisted.internet import defer
 
 
@@ -17,8 +16,6 @@ def get_lock_id_for_branch_id(branch_id):
 
 from lp.codehosting.puller.scheduler import LockError  # noqa: E402
 
-UTC = pytz.timezone("UTC")
-
 
 def mirror(logger, manager):
     """Mirror all current branches that need to be mirrored."""
@@ -28,10 +25,10 @@ def mirror(logger, manager):
         logger.info("Could not acquire lock: %s", exception)
         return defer.succeed(0)
 
-    date_started = datetime.datetime.now(UTC)
+    date_started = datetime.now(timezone.utc)
 
     def recordSuccess(ignored):
-        date_completed = datetime.datetime.now(UTC)
+        date_completed = datetime.now(timezone.utc)
         return manager.recordActivity(date_started, date_completed)
 
     def unlock(passed_through):
diff --git a/lib/lp/codehosting/scanner/tests/test_bzrsync.py b/lib/lp/codehosting/scanner/tests/test_bzrsync.py
index 290a304..ced2631 100644
--- a/lib/lp/codehosting/scanner/tests/test_bzrsync.py
+++ b/lib/lp/codehosting/scanner/tests/test_bzrsync.py
@@ -3,12 +3,11 @@
 # Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-import datetime
 import os
 import random
 import time
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from breezy.revision import NULL_REVISION
 from breezy.revision import Revision as BzrRevision
 from breezy.tests import TestCaseWithTransport
@@ -439,8 +438,7 @@ class TestBzrSync(BzrSyncTestCase):
         )
         rev_1 = IStore(Revision).find(Revision, revision_id="rev-1").one()
         rev_2 = IStore(Revision).find(Revision, revision_id="rev-2").one()
-        UTC = pytz.timezone("UTC")
-        dt = datetime.datetime.fromtimestamp(1000000000.0, UTC)
+        dt = datetime.fromtimestamp(1000000000.0, timezone.utc)
         self.assertEqual(rev_1.revision_date, dt)
         self.assertEqual(rev_2.revision_date, dt)
 
@@ -668,9 +666,8 @@ class TestBzrSyncRevisions(BzrSyncTestCase):
         # timestamps.
 
         # Make a negative, fractional timestamp and equivalent datetime
-        UTC = pytz.timezone("UTC")
         old_timestamp = -0.5
-        old_date = datetime.datetime(1969, 12, 31, 23, 59, 59, 500000, UTC)
+        old_date = datetime(1969, 12, 31, 23, 59, 59, 500000, timezone.utc)
 
         # Fake revision with negative timestamp.
         fake_rev = BzrRevision(
@@ -781,9 +778,7 @@ class TestGenerateIncrementalDiffJob(BzrSyncTestCase):
         )
         self.db_branch.last_scanned_id = parent_id.decode()
         # Make sure that the merge proposal is created in the past.
-        date_created = datetime.datetime.now(pytz.UTC) - datetime.timedelta(
-            days=7
-        )
+        date_created = datetime.now(timezone.utc) - timedelta(days=7)
         bmp = self.factory.makeBranchMergeProposal(
             source_branch=self.db_branch, date_created=date_created
         )
diff --git a/lib/lp/codehosting/scripts/modifiedbranches.py b/lib/lp/codehosting/scripts/modifiedbranches.py
index e111e17..2609c5c 100644
--- a/lib/lp/codehosting/scripts/modifiedbranches.py
+++ b/lib/lp/codehosting/scripts/modifiedbranches.py
@@ -7,10 +7,9 @@ __all__ = ["ModifiedBranchesScript"]
 
 
 import os
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from time import strptime
 
-import pytz
 from zope.component import getUtility
 
 from lp.code.enums import BranchType
@@ -95,7 +94,7 @@ class ModifiedBranchesScript(LaunchpadScript):
             )
 
         # Make the datetime timezone aware.
-        return last_modified.replace(tzinfo=pytz.UTC)
+        return last_modified.replace(tzinfo=timezone.utc)
 
     def branch_location(self, branch):
         """Return the  branch path for the given branch."""
diff --git a/lib/lp/codehosting/scripts/tests/test_modifiedbranches.py b/lib/lp/codehosting/scripts/tests/test_modifiedbranches.py
index 30f10eb..474d80a 100644
--- a/lib/lp/codehosting/scripts/tests/test_modifiedbranches.py
+++ b/lib/lp/codehosting/scripts/tests/test_modifiedbranches.py
@@ -4,9 +4,7 @@
 """Test the modified branches script."""
 
 import os
-from datetime import datetime
-
-import pytz
+from datetime import datetime, timezone
 
 from lp.code.enums import BranchType
 from lp.codehosting.scripts.modifiedbranches import ModifiedBranchesScript
@@ -61,7 +59,7 @@ class TestModifiedBranchesLastModifiedEpoch(TestCase):
             "modified-branches", test_args=["--since=2009-03-02"]
         )
         self.assertEqual(
-            datetime(2009, 3, 2, tzinfo=pytz.UTC),
+            datetime(2009, 3, 2, tzinfo=timezone.utc),
             script.get_last_modified_epoch(),
         )
 
@@ -82,10 +80,10 @@ class TestModifiedBranchesLastModifiedEpoch(TestCase):
         )
         # Override the script's now_timestamp to have a definitive test.
         # 3pm on the first of January.
-        script.now_timestamp = datetime(2009, 1, 1, 15, tzinfo=pytz.UTC)
+        script.now_timestamp = datetime(2009, 1, 1, 15, tzinfo=timezone.utc)
         # The last modified should be 3am on the same day.
         self.assertEqual(
-            datetime(2009, 1, 1, 3, tzinfo=pytz.UTC),
+            datetime(2009, 1, 1, 3, tzinfo=timezone.utc),
             script.get_last_modified_epoch(),
         )
 
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 19e2bf4..61ee483 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -3,11 +3,10 @@
 
 """Test OCI recipe views."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 from urllib.parse import quote
 
-import pytz
 import soupmatchers
 from fixtures import FakeLogger
 from storm.locals import Store
@@ -683,7 +682,7 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
             member_of=[getUtility(ILaunchpadCelebrities).ppa_admin]
         )
         login_person(self.person)
-        date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+        date_created = datetime(2000, 1, 1, tzinfo=timezone.utc)
         recipe = self.factory.makeOCIRecipe(
             registrant=self.person, date_created=date_created
         )
@@ -902,7 +901,7 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
 
     def test_edit_recipe_sets_date_last_modified(self):
         # Editing an OCI recipe sets the date_last_modified property.
-        date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+        date_created = datetime(2000, 1, 1, tzinfo=timezone.utc)
         recipe = self.factory.makeOCIRecipe(
             registrant=self.person, date_created=date_created
         )
@@ -1481,7 +1480,7 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
         if recipe is None:
             recipe = self.makeOCIRecipe()
         if date_created is None:
-            date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
+            date_created = datetime.now(timezone.utc) - timedelta(hours=1)
         return self.factory.makeOCIRecipeBuild(
             requester=self.person,
             recipe=recipe,
@@ -2083,7 +2082,7 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
         recipe = self.makeOCIRecipe()
         # Create oldest builds first so that they sort properly by id.
         date_gen = time_counter(
-            datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+            datetime(2000, 1, 1, tzinfo=timezone.utc), timedelta(days=1)
         )
         builds = [
             self.makeBuild(recipe=recipe, date_created=next(date_gen))
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 23bf584..6f2e772 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -3,8 +3,6 @@
 
 """A recipe for building Open Container Initiative images."""
 
-from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
-
 __all__ = [
     "get_ocirecipe_privacy_filter",
     "OCIRecipe",
@@ -12,8 +10,8 @@ __all__ = [
     "OCIRecipeSet",
 ]
 
+from datetime import timezone
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.databases.postgres import JSON
 from storm.expr import SQL, And, Coalesce, Desc, Exists, Join, Not, Or, Select
@@ -109,6 +107,7 @@ from lp.services.job.model.job import Job
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.webhooks.interfaces import IWebhookSet
 from lp.services.webhooks.model import WebhookTargetMixin
+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
 from lp.soyuz.model.distroarchseries import DistroArchSeries
 
 
@@ -128,10 +127,10 @@ class OCIRecipe(StormBase, WebhookTargetMixin):
 
     id = Int(primary=True)
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     registrant_id = Int(name="registrant", allow_none=False)
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index a3aa27e..7e2301a 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -9,9 +9,8 @@ __all__ = [
     "OCIRecipeBuildSet",
 ]
 
-from datetime import timedelta
+from datetime import timedelta, timezone
 
-import pytz
 from storm.expr import LeftJoin
 from storm.locals import (
     Bool,
@@ -92,7 +91,7 @@ class OCIFile(StormBase):
     layer_file_digest = Unicode(name="layer_file_digest", allow_none=True)
 
     date_last_used = DateTime(
-        name="date_last_used", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_used", tzinfo=timezone.utc, allow_none=False
     )
 
     def __init__(self, build, library_file, layer_file_digest=None):
@@ -137,12 +136,12 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
     virtualized = Bool(name="virtualized")
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
-    date_started = DateTime(name="date_started", tzinfo=pytz.UTC)
-    date_finished = DateTime(name="date_finished", tzinfo=pytz.UTC)
+    date_started = DateTime(name="date_started", tzinfo=timezone.utc)
+    date_finished = DateTime(name="date_finished", tzinfo=timezone.utc)
     date_first_dispatched = DateTime(
-        name="date_first_dispatched", tzinfo=pytz.UTC
+        name="date_first_dispatched", tzinfo=timezone.utc
     )
 
     builder_id = Int(name="builder")
diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
index 5f026df..8165c82 100644
--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
@@ -13,10 +13,9 @@ __all__ = [
 
 import json
 import os
-from datetime import datetime
+from datetime import datetime, timezone
 from typing import Any, Generator
 
-import pytz
 from twisted.internet import defer
 from zope.component import getUtility
 from zope.interface import implementer
@@ -224,7 +223,7 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
                 if oci_file:
                     librarian_file = oci_file.library_file
                     unsecure_file = removeSecurityProxy(oci_file)
-                    unsecure_file.date_last_used = datetime.now(pytz.UTC)
+                    unsecure_file.date_last_used = datetime.now(timezone.utc)
                 # If it doesn't, we need to download it
                 else:
                     files.add(layer_filename)
diff --git a/lib/lp/oci/model/ocirecipesubscription.py b/lib/lp/oci/model/ocirecipesubscription.py
index ba657cf..4686a73 100644
--- a/lib/lp/oci/model/ocirecipesubscription.py
+++ b/lib/lp/oci/model/ocirecipesubscription.py
@@ -5,7 +5,8 @@
 
 __all__ = ["OCIRecipeSubscription"]
 
-import pytz
+from datetime import timezone
+
 from storm.properties import DateTime, Int
 from storm.references import Reference
 from zope.interface import implementer
@@ -31,7 +32,9 @@ class OCIRecipeSubscription(StormBase):
     recipe_id = Int("recipe", allow_none=False)
     recipe = Reference(recipe_id, "OCIRecipe.id")
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     subscribed_by_id = Int(
         "subscribed_by", allow_none=False, validator=validate_person
diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
index be8e24c..e51ce67 100644
--- a/lib/lp/oci/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
@@ -3,10 +3,9 @@
 
 """Tests for OCI image building recipe functionality."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.request import urlopen
 
-import pytz
 from fixtures import FakeLogger
 from pymacaroons import Macaroon
 from testtools.matchers import (
@@ -212,7 +211,7 @@ class TestOCIRecipeBuild(OCIConfigHelperMixin, TestCaseWithFactory):
     def test_retry_resets_state(self):
         # Retrying a build resets most of the state attributes, but does
         # not modify the first dispatch time.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         build = self.factory.makeOCIRecipeBuild()
         build.updateStatus(BuildStatus.BUILDING, date_started=now)
         build.updateStatus(BuildStatus.FAILEDTOBUILD)
diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
index 27bf8b5..8e2c214 100644
--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
@@ -10,11 +10,10 @@ import shutil
 import tempfile
 import time
 import uuid
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import urlsplit
 
 import fixtures
-import pytz
 import six
 from fixtures import MockPatch
 from pymacaroons import Macaroon
@@ -1053,7 +1052,7 @@ class TestHandleStatusForOCIRecipeBuild(
             content=b"layer 2 retrieved from librarian",
         )
 
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         mock_datetime = self.useFixture(
             MockPatch("lp.buildmaster.model.buildfarmjobbehaviour.datetime")
         ).mock
diff --git a/lib/lp/registry/browser/distributionmirror.py b/lib/lp/registry/browser/distributionmirror.py
index 0876547..1f86eab 100644
--- a/lib/lp/registry/browser/distributionmirror.py
+++ b/lib/lp/registry/browser/distributionmirror.py
@@ -13,9 +13,8 @@ __all__ = [
     "DistributionMirrorBreadcrumb",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.event import notify
 from zope.interface import implementer
 from zope.lifecycleevent import ObjectCreatedEvent
@@ -297,7 +296,7 @@ class DistributionMirrorReviewView(LaunchpadEditFormView):
         context = self.context
         if data["status"] != context.status:
             context.reviewer = self.user
-            context.date_reviewed = datetime.now(pytz.timezone("UTC"))
+            context.date_reviewed = datetime.now(timezone.utc)
         self.updateContextFromData(data)
         self.next_url = canonical_url(context)
 
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index fc12a73..1484dab 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -50,7 +50,7 @@ __all__ = [
 
 
 import itertools
-from datetime import datetime
+from datetime import datetime, timezone
 from itertools import chain
 from operator import attrgetter, itemgetter
 from textwrap import dedent
@@ -3946,7 +3946,7 @@ class PersonOAuthTokensView(LaunchpadView):
                 "Invalid form value for token_type: %r" % token_type
             )
         if token is not None:
-            token.date_expires = datetime.now(pytz.timezone("UTC"))
+            token.date_expires = datetime.now(timezone.utc)
             self.request.response.addInfoNotification(
                 "Authorization revoked successfully."
             )
@@ -4419,13 +4419,13 @@ class PersonEditTimeZoneView(LaunchpadFormView):
     @action(_("Update"), name="update")
     def action_update(self, action, data):
         """Set the time zone for the person."""
-        timezone = data.get("time_zone")
-        if timezone is None:
+        tz = data.get("time_zone")
+        if tz is None:
             raise UnexpectedFormData("No location received.")
         # XXX salgado, 2012-02-16, bug=933699: Use setLocation() because it's
         # the cheaper way to set the timezone of a person. Once the bug is
         # fixed we'll be able to get rid of this hack.
-        self.context.setLocation(None, None, timezone, self.user)
+        self.context.setLocation(None, None, tz, self.user)
 
 
 def archive_to_person(archive):
diff --git a/lib/lp/registry/browser/team.py b/lib/lp/registry/browser/team.py
index 386c325..079a284 100644
--- a/lib/lp/registry/browser/team.py
+++ b/lib/lp/registry/browser/team.py
@@ -32,10 +32,9 @@ __all__ = [
 
 import json
 import math
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from urllib.parse import unquote
 
-import pytz
 from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache
 from lazr.restful.utils import smartquote
@@ -1420,7 +1419,7 @@ class TeamMembershipSelfRenewalView(LaunchpadFormView):
         # cover the fencepost error when `date_limit` is
         # earlier than `self.dateexpires`, which happens later
         # in the same day.
-        date_limit = datetime.now(pytz.UTC) + timedelta(
+        date_limit = datetime.now(timezone.utc) + timedelta(
             days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT + 1
         )
         if context.status not in (admin, approved):
@@ -1457,7 +1456,7 @@ class TeamMembershipSelfRenewalView(LaunchpadFormView):
 
     @property
     def time_before_expiration(self):
-        return self.context.dateexpires - datetime.now(pytz.timezone("UTC"))
+        return self.context.dateexpires - datetime.now(timezone.utc)
 
     @property
     def next_url(self):
diff --git a/lib/lp/registry/browser/teammembership.py b/lib/lp/registry/browser/teammembership.py
index ed2942c..e554738 100644
--- a/lib/lp/registry/browser/teammembership.py
+++ b/lib/lp/registry/browser/teammembership.py
@@ -8,9 +8,8 @@ __all__ = [
 ]
 
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.formlib import form
 from zope.formlib.interfaces import InputErrors
 from zope.formlib.widget import CustomWidgetFactory
@@ -43,7 +42,6 @@ class TeamMembershipEditView(LaunchpadView):
         expiration_field = fields["expirationdate"]
         expiration_field.custom_widget = CustomWidgetFactory(DateWidget)
         expires = self.context.dateexpires
-        UTC = pytz.timezone("UTC")
         if self.isExpired():
             # For expired members, we will present the team's default
             # renewal date.
@@ -52,7 +50,7 @@ class TeamMembershipEditView(LaunchpadView):
             # For members who were deactivated, we present by default
             # their original expiration date, or, if that has passed, or
             # never set, the team's default renewal date.
-            if expires is None or expires < datetime.now(UTC):
+            if expires is None or expires < datetime.now(timezone.utc):
                 expires = self.context.team.defaultrenewedexpirationdate
         if expires is not None:
             # We get a datetime from the database, but we want to use a
@@ -69,7 +67,7 @@ class TeamMembershipEditView(LaunchpadView):
         )
         self.expiration_widget = self.widgets["expirationdate"]
         # Set the acceptable date range for expiration.
-        self.expiration_widget.from_date = datetime.now(UTC).date()
+        self.expiration_widget.from_date = datetime.now(timezone.utc).date()
         # Disable the date widget if there is no current or required
         # expiration
         if not expires:
@@ -332,9 +330,8 @@ class TeamMembershipEditView(LaunchpadView):
 
         # We used a date picker, so we have a date. What we want is a
         # datetime in UTC
-        UTC = pytz.timezone("UTC")
         expires = datetime(
-            expires.year, expires.month, expires.day, tzinfo=UTC
+            expires.year, expires.month, expires.day, tzinfo=timezone.utc
         )
         return expires
 
diff --git a/lib/lp/registry/browser/tests/distributionmirror-views.rst b/lib/lp/registry/browser/tests/distributionmirror-views.rst
index 86ec5c5..6791485 100644
--- a/lib/lp/registry/browser/tests/distributionmirror-views.rst
+++ b/lib/lp/registry/browser/tests/distributionmirror-views.rst
@@ -242,9 +242,8 @@ changed.
     # but since this test could run at 23:59:59 of any given day we can only
     # reliably check that the timedelta from now to the date it was reviewed
     # is less than or equal to 1 day.
-    >>> import pytz
-    >>> from datetime import datetime
-    >>> utc_now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timezone
+    >>> utc_now = datetime.now(timezone.utc)
     >>> abs((mirror.date_reviewed.date() - utc_now.date()).days) <= 1
     True
 
diff --git a/lib/lp/registry/browser/tests/distributionsourcepackage-views.rst b/lib/lp/registry/browser/tests/distributionsourcepackage-views.rst
index caad996..83b2ff2 100644
--- a/lib/lp/registry/browser/tests/distributionsourcepackage-views.rst
+++ b/lib/lp/registry/browser/tests/distributionsourcepackage-views.rst
@@ -14,13 +14,12 @@ package within a distribution.
     >>> publisher.distroseries.status = SeriesStatus.DEVELOPMENT
 
     # Publish the source 'gedit' in the ubuntutest main archive.
-    >>> from datetime import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
     >>> from lp.soyuz.enums import PackagePublishingStatus
     >>> gedit_main_src_hist = publisher.getPubSource(
     ...     sourcename="gedit",
     ...     archive=ubuntutest.main_archive,
-    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=pytz.UTC),
+    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=timezone.utc),
     ...     status=PackagePublishingStatus.PUBLISHED,
     ... )
 
@@ -30,7 +29,7 @@ series.
     >>> eel_main_src_hist = publisher.getPubSource(
     ...     sourcename="eel",
     ...     archive=ubuntutest.main_archive,
-    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=pytz.UTC),
+    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=timezone.utc),
     ...     status=PackagePublishingStatus.PUBLISHED,
     ...     distroseries=ubuntutest.getSeries("breezy-autotest"),
     ... )
@@ -40,7 +39,7 @@ series.
     >>> eel_main_src_hist = publisher.getPubSource(
     ...     sourcename="eel",
     ...     archive=ubuntutest.main_archive,
-    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=pytz.UTC),
+    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=timezone.utc),
     ...     status=PackagePublishingStatus.PUBLISHED,
     ...     distroseries=earliest_series,
     ... )
@@ -50,7 +49,7 @@ series.
     >>> eel_main_src_hist = publisher.getPubSource(
     ...     sourcename="eel",
     ...     archive=ubuntutest.main_archive,
-    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=pytz.UTC),
+    ...     date_uploaded=datetime(2010, 12, 30, tzinfo=timezone.utc),
     ...     status=PackagePublishingStatus.PUBLISHED,
     ...     distroseries=latest_series,
     ... )
diff --git a/lib/lp/registry/browser/tests/milestone-views.rst b/lib/lp/registry/browser/tests/milestone-views.rst
index ab65479..7103470 100644
--- a/lib/lp/registry/browser/tests/milestone-views.rst
+++ b/lib/lp/registry/browser/tests/milestone-views.rst
@@ -707,11 +707,12 @@ The view will delete the dependent product release and release files if
 they exist. It will also untarget bugtasks and specifications from the
 milestone.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timezone
 
     >>> milestone = firefox_1_0.newMilestone("1.0.11")
-    >>> release = milestone.createProductRelease(owner, datetime.now(UTC))
+    >>> release = milestone.createProductRelease(
+    ...     owner, datetime.now(timezone.utc)
+    ... )
     >>> release_file = release.addReleaseFile(
     ...     "test", b"test", "text/plain", owner, description="test file"
     ... )
diff --git a/lib/lp/registry/browser/tests/poll-views_0.rst b/lib/lp/registry/browser/tests/poll-views_0.rst
index 238ea58..9b0f7e9 100644
--- a/lib/lp/registry/browser/tests/poll-views_0.rst
+++ b/lib/lp/registry/browser/tests/poll-views_0.rst
@@ -3,8 +3,7 @@ Poll Pages
 
 First import some stuff and setup some things we'll use in this test.
 
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.component import getUtility, getMultiAdapter
     >>> from zope.publisher.browser import TestRequest
     >>> from lp.registry.interfaces.person import IPersonSet
@@ -29,7 +28,7 @@ with a proper explanation of why it failed.
     ...     "%Y-%m-%d %H:%M:%S"
     ... )
     >>> one_year_from_now = (
-    ...     datetime.now(pytz.UTC) + timedelta(days=365)
+    ...     datetime.now(timezone.utc) + timedelta(days=365)
     ... ).strftime("%Y-%m-%d")
     >>> form = {
     ...     "field.name": "test-poll",
diff --git a/lib/lp/registry/browser/tests/productseries-views.rst b/lib/lp/registry/browser/tests/productseries-views.rst
index 6909569..32d636f 100644
--- a/lib/lp/registry/browser/tests/productseries-views.rst
+++ b/lib/lp/registry/browser/tests/productseries-views.rst
@@ -211,12 +211,11 @@ Users with edit permission may delete a project's series. This person is
 often the project's owner or series driver who has setup the series by
 mistake.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timezone
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> celebrities = getUtility(ILaunchpadCelebrities)
 
-    >>> test_date = datetime(2009, 5, 1, 19, 34, 24, tzinfo=UTC)
+    >>> test_date = datetime(2009, 5, 1, 19, 34, 24, tzinfo=timezone.utc)
     >>> product = factory.makeProduct(name="field", displayname="Field")
     >>> productseries = factory.makeProductSeries(
     ...     product=product, name="rabbit", date_created=test_date
diff --git a/lib/lp/registry/browser/tests/test_announcements.py b/lib/lp/registry/browser/tests/test_announcements.py
index 260eec2..1c20876 100644
--- a/lib/lp/registry/browser/tests/test_announcements.py
+++ b/lib/lp/registry/browser/tests/test_announcements.py
@@ -3,10 +3,9 @@
 
 """Tests for +announcement views."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
 from lxml import html
-from pytz import utc
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -43,7 +42,7 @@ class TestAnnouncement(TestCaseWithFactory):
     def test_announcement_info_with_publication_date(self):
         product = self.factory.makeProduct(displayname="Foo")
         announcer = self.factory.makePerson(displayname="Bar Baz")
-        announced = datetime(2007, 1, 12, tzinfo=utc)
+        announced = datetime(2007, 1, 12, tzinfo=timezone.utc)
         announcement = product.announce(
             announcer, "Hello World", publication_date=announced
         )
@@ -98,7 +97,7 @@ class TestAnnouncementPage(BrowserTestCase):
             real_user,
             "Some real announcement",
             "Yep, announced here",
-            publication_date=datetime.now(utc),
+            publication_date=datetime.now(timezone.utc),
         )
 
         second_product = self.factory.makeProduct(
@@ -108,7 +107,7 @@ class TestAnnouncementPage(BrowserTestCase):
             team,
             "Other real announcement",
             "Yep too, announced here",
-            publication_date=datetime.now(utc),
+            publication_date=datetime.now(timezone.utc),
         )
 
         inactive_product = self.factory.makeProduct(
@@ -118,7 +117,7 @@ class TestAnnouncementPage(BrowserTestCase):
             real_user,
             "Do not show inactive, please",
             "Nope, not here",
-            publication_date=datetime.now(utc),
+            publication_date=datetime.now(timezone.utc),
         )
         removeSecurityProxy(inactive_product).active = False
 
diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
index 76066f3..112909e 100644
--- a/lib/lp/registry/browser/tests/test_ociproject.py
+++ b/lib/lp/registry/browser/tests/test_ociproject.py
@@ -6,9 +6,8 @@
 __all__ = []
 
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import soupmatchers
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -544,7 +543,7 @@ class TestOCIProjectEditView(BrowserTestCase):
 
     def test_edit_oci_project_sets_date_last_modified(self):
         # Editing an OCI project sets the date_last_modified property.
-        date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+        date_created = datetime(2000, 1, 1, tzinfo=timezone.utc)
         oci_project = self.factory.makeOCIProject(date_created=date_created)
         self.assertEqual(date_created, oci_project.date_last_modified)
         with person_logged_in(oci_project.pillar.owner):
diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
index 0b08152..9f35cc4 100644
--- a/lib/lp/registry/browser/tests/test_person.py
+++ b/lib/lp/registry/browser/tests/test_person.py
@@ -4,12 +4,11 @@
 import doctest
 import email
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 from textwrap import dedent
 from urllib.parse import urljoin
 
-import pytz
 import soupmatchers
 import transaction
 from fixtures import FakeLogger
@@ -1255,7 +1254,7 @@ class TestPersonParticipationView(TestCaseWithFactory):
 
         membership_set = getUtility(ITeamMembershipSet)
         membership = membership_set.getByPersonAndTeam(self.user, team)
-        tomorrow = datetime.now(pytz.timezone("UTC")) + timedelta(days=1)
+        tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
         with person_logged_in(self.user):
             membership.setExpirationDate(tomorrow, self.user)
         view = create_view(self.user, name="+participation")
diff --git a/lib/lp/registry/browser/tests/test_poll.py b/lib/lp/registry/browser/tests/test_poll.py
index b70c036..1192895 100644
--- a/lib/lp/registry/browser/tests/test_poll.py
+++ b/lib/lp/registry/browser/tests/test_poll.py
@@ -4,9 +4,8 @@
 """Tests for IPoll views."""
 
 import os
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from fixtures import FakeLogger
 
 from lp.registry.interfaces.poll import CannotCreatePoll, PollAlgorithm
@@ -65,7 +64,7 @@ class TestPollAddView(BrowserTestCase):
         self.useFixture(FakeLogger())
         new_person = self.factory.makePerson()
         team = self.factory.makeTeam(owner=new_person)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         browser = self.getViewBrowser(
             team, view_name="+newpoll", user=new_person
         )
@@ -88,7 +87,7 @@ class TestPollAddView(BrowserTestCase):
         # A user with some kind of track record can create polls.
         person = self.factory.makePerson(karma=10)
         team = self.factory.makeTeam(owner=person)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         browser = self.getViewBrowser(team, view_name="+newpoll", user=person)
         browser.getControl("The unique name of this poll").value = "colour"
         browser.getControl("The title of this poll").value = "Favourite Colour"
diff --git a/lib/lp/registry/doc/announcement.rst b/lib/lp/registry/doc/announcement.rst
index 418a1d6..89590fb 100644
--- a/lib/lp/registry/doc/announcement.rst
+++ b/lib/lp/registry/doc/announcement.rst
@@ -10,8 +10,7 @@ specified date or until manually approved. Announcements can be retracted
 after publishing, and they can be deleted, permanently.
 
     >>> from zope.component import getUtility
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.services.utils import utc_now
     >>> NOW = utc_now()
     >>> FUTURE = NOW + timedelta(days=10)
@@ -54,7 +53,7 @@ published:
     ...         "http://www.mail-archive.com/announce@xxxxxxxxxx/msg00369.html";
     ...     ),
     ...     publication_date=datetime(
-    ...         2007, 7, 12, 11, 17, 39, tzinfo=pytz.utc
+    ...         2007, 7, 12, 11, 17, 39, tzinfo=timezone.utc
     ...     ),
     ... )
 
@@ -83,7 +82,9 @@ Announcements can also be made on an IProduct:
     ...     summary="""We just finished an excellent series of discussions on
     ...  performance at ApacheCon, and there is a summary of our current plans
     ...  available online. Please feel free to review and comment!""",
-    ...     publication_date=datetime(2006, 6, 30, 9, 0, 0, tzinfo=pytz.utc),
+    ...     publication_date=datetime(
+    ...         2006, 6, 30, 9, 0, 0, tzinfo=timezone.utc
+    ...     ),
     ... )
 
 
@@ -124,7 +125,9 @@ And finally, we can make announcements on an IDistribution, too:
     ...  have pushed Kubuntu 7.10 to mirrors and published the final packages
     ...  in the archive. Go ahead and fire up your Torrent client for the
     ...  latest in KDE goodness!""",
-    ...     publication_date=datetime(2007, 11, 3, 7, 0, 0, tzinfo=pytz.utc),
+    ...     publication_date=datetime(
+    ...         2007, 11, 3, 7, 0, 0, tzinfo=timezone.utc
+    ...     ),
     ... )
 
 Let's flush these to the database.
@@ -260,7 +263,7 @@ Announcements which have been retracted can be published again:
     >>> apache_asia.published
     False
     >>> apache_asia.setPublicationDate(
-    ...     datetime(2007, 11, 11, 7, 0, 0, tzinfo=pytz.utc)
+    ...     datetime(2007, 11, 11, 7, 0, 0, tzinfo=timezone.utc)
     ... )
     >>> apache_asia.published
     True
diff --git a/lib/lp/registry/doc/commercialsubscription.rst b/lib/lp/registry/doc/commercialsubscription.rst
index 6183965..6912fe4 100644
--- a/lib/lp/registry/doc/commercialsubscription.rst
+++ b/lib/lp/registry/doc/commercialsubscription.rst
@@ -67,14 +67,13 @@ or if the licence has been reviewed and been manually approved.
 
 The commercial subscription is about to expire here.
 
-    >>> from datetime import date, datetime, timedelta
-    >>> from pytz import UTC
+    >>> from datetime import date, datetime, timedelta, timezone
     >>> from zope.security.proxy import removeSecurityProxy
     >>> from lp.registry.interfaces.product import License
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> bzr.licenses = [License.OTHER_PROPRIETARY]
     >>> subscription = removeSecurityProxy(bzr.commercial_subscription)
-    >>> subscription.date_expires = datetime.now(UTC) + timedelta(29)
+    >>> subscription.date_expires = datetime.now(timezone.utc) + timedelta(29)
     >>> bzr.qualifies_for_free_hosting
     False
     >>> bzr.commercial_subscription_is_due
@@ -87,7 +86,7 @@ The commercial subscription is about to expire here.
 The subscription will not expire for more than 30 days so a new
 subscription is not due yet.
 
-    >>> subscription.date_expires = datetime.now(UTC) + timedelta(31)
+    >>> subscription.date_expires = datetime.now(timezone.utc) + timedelta(31)
     >>> bzr.commercial_subscription_is_due
     False
 
@@ -202,7 +201,6 @@ as well as the license_info field. The results are ordered by date created
 then display name.
 
     >>> from lp.services.database.sqlbase import flush_database_updates
-    >>> from datetime import timedelta
     >>> bzr.licenses = [License.GNU_GPL_V2, License.ECLIPSE]
     >>> flush_database_updates()
     >>> for product in product_set.forReview(
diff --git a/lib/lp/registry/doc/distribution-mirror.rst b/lib/lp/registry/doc/distribution-mirror.rst
index 17f4e79..baadc6e 100644
--- a/lib/lp/registry/doc/distribution-mirror.rst
+++ b/lib/lp/registry/doc/distribution-mirror.rst
@@ -4,7 +4,7 @@ Distribution Mirrors
 A distribution mirror must always be associated with a single distribution, so
 to create a new mirror you should use the Distribution.newMirror method.
 
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.component import getUtility
     >>> from lp.services.librarian.interfaces import ILibraryFileAliasSet
     >>> from lp.registry.interfaces.distribution import IDistributionSet
@@ -358,9 +358,7 @@ probed as we don't care about it anyway.
     >>> mirrors = mirrorset.getMirrorsToProbe(
     ...     MirrorContent.ARCHIVE, ignore_last_probe=True
     ... )
-    >>> import pytz
-    >>> utc = pytz.timezone("UTC")
-    >>> now = datetime.now(utc)
+    >>> now = datetime.now(timezone.utc)
     >>> for mirror in mirrors:
     ...     last_probe = mirror.last_probe_record
     ...     last_probe_date = "NEVER"
@@ -624,7 +622,7 @@ checked if that mirror's last sync was in the last one or two days.
     ...     warty, PackagePublishingPocket.RELEASE, warty.components[0]
     ... )
 
-    >>> when = datetime(2005, 9, 17, tzinfo=utc)
+    >>> when = datetime(2005, 9, 17, tzinfo=timezone.utc)
     >>> urls = warty_mirror.getURLsToCheckUpdateness(when=when)
     >>> for freshness, url in urls.items():
     ...     print("%s: %s" % (freshness.name, url))  # noqa
@@ -675,8 +673,10 @@ so we need to skip that upload.
     ... )
     >>> warty_i386_mirror = removeSecurityProxy(warty_i386_mirror)
     >>> recent_freshness, threshold = warty_i386_mirror.freshness_times[0]
-    >>> start = datetime(2005, 6, 20, tzinfo=utc)
-    >>> end = datetime(2005, 6, 20, tzinfo=utc) + timedelta(hours=0.5)
+    >>> start = datetime(2005, 6, 20, tzinfo=timezone.utc)
+    >>> end = datetime(2005, 6, 20, tzinfo=timezone.utc) + timedelta(
+    ...     hours=0.5
+    ... )
     >>> time_interval = (start, end)
     >>> upload = warty_i386_mirror.getLatestPublishingEntry(
     ...     time_interval, deb_only=False
@@ -695,7 +695,7 @@ so we need to skip that upload.
     >>> print(bpf.filetype.title)
     UDEB Format
 
-    >>> when = datetime(2005, 6, 22, tzinfo=utc)
+    >>> when = datetime(2005, 6, 22, tzinfo=timezone.utc)
     >>> urls = warty_i386_mirror.getURLsToCheckUpdateness(when=when)
     >>> for freshness, url in urls.items():
     ...     print("%s: %s" % (freshness.name, url))  # noqa
@@ -704,7 +704,7 @@ so we need to skip that upload.
     ONEWEEKBEHIND:
     http://foo.bar.com/pub/pool/main/m/mozilla-firefox/mozilla-firefox_0.9_i386.deb
 
-    >>> when = datetime(2005, 6, 20, 0, 1, tzinfo=utc)
+    >>> when = datetime(2005, 6, 20, 0, 1, tzinfo=timezone.utc)
     >>> urls = warty_i386_mirror.getURLsToCheckUpdateness(when=when)
     >>> for freshness, url in urls.items():
     ...     print("%s: %s" % (freshness.name, url))  # noqa
diff --git a/lib/lp/registry/doc/milestone.rst b/lib/lp/registry/doc/milestone.rst
index c784252..7f8effb 100644
--- a/lib/lp/registry/doc/milestone.rst
+++ b/lib/lp/registry/doc/milestone.rst
@@ -313,7 +313,7 @@ values of the product milestones.
     >>> print(gnome.getMilestone("1.1").dateexpected)
     None
 
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> applets_1_1.dateexpected = datetime(2007, 4, 2)
     >>> print(gnome.getMilestone("1.1").dateexpected)
     2007-04-02 00:00:00
@@ -514,11 +514,10 @@ specifications targeted to it.
 If a milestone has a product release associated with it though, it can
 not be deleted.
 
-    >>> from datetime import datetime
-    >>> from pytz import UTC
-
     >>> milestone = ff_onedotzero.newMilestone("1.0.11")
-    >>> release = milestone.createProductRelease(owner, datetime.now(UTC))
+    >>> release = milestone.createProductRelease(
+    ...     owner, datetime.now(timezone.utc)
+    ... )
     >>> milestone.destroySelf()
     Traceback (most recent call last):
     ...
@@ -587,7 +586,9 @@ event is signaled for each changed bug task.
     >>> triaged_bugtask = factory.makeBugTask(target=upstream_firefox)
     >>> triaged_bugtask.transitionToMilestone(milestone, owner)
     >>> triaged_bugtask.transitionToStatus(BugTaskStatus.TRIAGED, owner)
-    >>> release = milestone.createProductRelease(owner, datetime.now(UTC))
+    >>> release = milestone.createProductRelease(
+    ...     owner, datetime.now(timezone.utc)
+    ... )
     >>> bugtask_event_listener = ZopeEventHandlerFixture(
     ...     print_event, (IBugTask, IObjectModifiedEvent)
     ... )
diff --git a/lib/lp/registry/doc/person-merge.rst b/lib/lp/registry/doc/person-merge.rst
index ce13874..b1458b1 100644
--- a/lib/lp/registry/doc/person-merge.rst
+++ b/lib/lp/registry/doc/person-merge.rst
@@ -435,15 +435,14 @@ people.  Note, though, that when merging teams, its polls will not be
 carried over to the remaining team.  Team memberships, on the other
 hand, are carried over just like when merging people.
 
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
     >>> from lp.registry.interfaces.poll import IPollSubset, PollSecrecy
     >>> test_team = personset.newTeam(sample, "test-team", "Test team")
     >>> launchpad_devs = personset.getByName("launchpad")
     >>> ignored = launchpad_devs.addMember(
     ...     test_team, reviewer=launchpad_devs.teamowner, force_team_add=True
     ... )
-    >>> today = datetime.now(pytz.timezone("UTC"))
+    >>> today = datetime.now(timezone.utc)
     >>> tomorrow = today + timedelta(days=1)
     >>> poll = IPollSubset(test_team).new(
     ...     "test-poll",
diff --git a/lib/lp/registry/doc/person-notification.rst b/lib/lp/registry/doc/person-notification.rst
index 584f46e..3751d2d 100644
--- a/lib/lp/registry/doc/person-notification.rst
+++ b/lib/lp/registry/doc/person-notification.rst
@@ -41,9 +41,8 @@ getNotificationsToSend().
 
 We can also retrieve notifications that are older than a certain date.
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> for n in notification_set.getNotificationsOlderThan(now):
     ...     print(n.subject)
     ...
diff --git a/lib/lp/registry/doc/person.rst b/lib/lp/registry/doc/person.rst
index ef62868..0e910b6 100644
--- a/lib/lp/registry/doc/person.rst
+++ b/lib/lp/registry/doc/person.rst
@@ -919,11 +919,10 @@ Searching only for People based on their names or email addresses:
 The created_before and created_after arguments can be used to restrict
 the matches by the IPerson.datecreated value.
 
-    >>> from datetime import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
 
-    >>> created_after = datetime(2008, 6, 27, tzinfo=pytz.UTC)
-    >>> created_before = datetime(2008, 7, 1, tzinfo=pytz.UTC)
+    >>> created_after = datetime(2008, 6, 27, tzinfo=timezone.utc)
+    >>> created_before = datetime(2008, 7, 1, tzinfo=timezone.utc)
     >>> print_people(
     ...     personset.findPerson(
     ...         text="",
diff --git a/lib/lp/registry/doc/poll.rst b/lib/lp/registry/doc/poll.rst
index 66e1917..8dd9714 100644
--- a/lib/lp/registry/doc/poll.rst
+++ b/lib/lp/registry/doc/poll.rst
@@ -8,8 +8,7 @@ like the 'Gnome Team' and the 'Ubuntu Team'. These teams often have leaders
 whose ellection depends on the vote of all members, and this is one of the
 reasons why we teams can have polls attached to them.
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.component import getUtility
     >>> from lp.services.database.sqlbase import flush_database_updates
     >>> from lp.testing import login
@@ -27,7 +26,7 @@ reasons why we teams can have polls attached to them.
     >>> member4 = getUtility(IPersonSet).getByName("name16")
     >>> member5 = getUtility(IPersonSet).getByName("limi")
     >>> nonmember = getUtility(IPersonSet).getByName("justdave")
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> now = datetime.now(timezone.utc)
     >>> onesec = timedelta(seconds=1)
 
 We need to login with one of the administrators of the team named
@@ -39,7 +38,7 @@ a given team (in our case, the 'Ubuntu Team')
     >>> pollsubset = IPollSubset(team)
 
 Now we create a new poll on this team.
-    >>> opendate = datetime(2005, 1, 1, tzinfo=pytz.timezone("UTC"))
+    >>> opendate = datetime(2005, 1, 1, tzinfo=timezone.utc)
     >>> closedate = opendate + timedelta(weeks=2)
     >>> title = "2005 Leader's Elections"
     >>> proposition = "Who's going to be the next leader?"
diff --git a/lib/lp/registry/doc/productrelease-file-download.rst b/lib/lp/registry/doc/productrelease-file-download.rst
index 746f284..1a5ceaa 100644
--- a/lib/lp/registry/doc/productrelease-file-download.rst
+++ b/lib/lp/registry/doc/productrelease-file-download.rst
@@ -49,9 +49,8 @@ Add a file alias to the productrelease.
     >>> from lp.services.webapp.interfaces import ILaunchBag
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timedelta, timezone
     >>> from io import BytesIO
-    >>> from pytz import UTC
     >>> from zope.security.proxy import removeSecurityProxy
     >>> def add_release_file(
     ...     release, file_content, name, description, date_uploaded=None
@@ -172,8 +171,7 @@ the date each was uploaded in reverse order.
 
 Let's add some release files to the releases for firefox.
 
-    >>> from datetime import timedelta
-    >>> now = datetime.now(UTC)
+    >>> now = datetime.now(timezone.utc)
     >>> for i, release in enumerate(releases):
     ...     content = b"Content %d" % i
     ...     name = "name%d" % i
diff --git a/lib/lp/registry/doc/productrelease.rst b/lib/lp/registry/doc/productrelease.rst
index e77970b..4a113ba 100644
--- a/lib/lp/registry/doc/productrelease.rst
+++ b/lib/lp/registry/doc/productrelease.rst
@@ -13,9 +13,10 @@ IMilestone.
     >>> owner = firefox_1_0.owner
     >>> ignored = login_person(owner)
     >>> milestone = firefox_1_0.newMilestone("1.0.9")
-    >>> from datetime import datetime
-    >>> from pytz import UTC
-    >>> firefox_109 = milestone.createProductRelease(owner, datetime.now(UTC))
+    >>> from datetime import datetime, timezone
+    >>> firefox_109 = milestone.createProductRelease(
+    ...     owner, datetime.now(timezone.utc)
+    ... )
     >>> from lp.registry.interfaces.productrelease import IProductRelease
     >>> verifyObject(IProductRelease, firefox_109)
     True
@@ -54,7 +55,7 @@ deleted.
 
     >>> milestone = firefox_1_0.newMilestone("1.0.10")
     >>> firefox_1010 = milestone.createProductRelease(
-    ...     owner, datetime.now(UTC)
+    ...     owner, datetime.now(timezone.utc)
     ... )
     >>> firefox_1010.addReleaseFile("test", b"test", "text/plain", owner)
     <ProductReleaseFile...
diff --git a/lib/lp/registry/doc/teammembership-email-notification.rst b/lib/lp/registry/doc/teammembership-email-notification.rst
index 5123960..bfabf4f 100644
--- a/lib/lp/registry/doc/teammembership-email-notification.rst
+++ b/lib/lp/registry/doc/teammembership-email-notification.rst
@@ -616,9 +616,8 @@ permissions) in case they want to retain that membership. This is done by
 the flag-expired-memberships cronscript, which uses
 ITeamMembership.sendExpirationWarningEmail to do its job.
 
-    >>> import pytz
-    >>> from datetime import datetime, timedelta
-    >>> utc_now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> utc_now = datetime.now(timezone.utc)
 
 In the case of the beta-testers team, the email is sent only to the
 team's owner, who doesn't have the necessary rights to renew the
@@ -798,7 +797,7 @@ notification is sent to all team admins.
     >>> karl_on_mirroradmins = membershipset.getByPersonAndTeam(
     ...     karl, mirror_admins
     ... )
-    >>> tomorrow = datetime.now(pytz.timezone("UTC")) + timedelta(days=1)
+    >>> tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
     >>> print(karl_on_mirroradmins.status.title)
     Approved
 
diff --git a/lib/lp/registry/doc/teammembership.rst b/lib/lp/registry/doc/teammembership.rst
index cac006e..a0fcd9a 100644
--- a/lib/lp/registry/doc/teammembership.rst
+++ b/lib/lp/registry/doc/teammembership.rst
@@ -11,7 +11,6 @@ represents all the people who are /effective members/ of the team.
 
 First of all, create some teams:
 
-    >>> import pytz
     >>> from datetime import datetime, timedelta, timezone
     >>> from lp.registry.interfaces.person import (
     ...     TeamMembershipRenewalPolicy,
@@ -563,7 +562,7 @@ was approved. It returns True to indicate that the status was changed.
     True
     >>> print(membership.status.title)
     Approved
-    >>> utc_now = datetime.now(pytz.timezone("UTC"))
+    >>> utc_now = datetime.now(timezone.utc)
     >>> membership.datejoined.date() == utc_now.date()
     True
 
@@ -614,8 +613,8 @@ the new expiry date is not in the past.
     True
     >>> foobar_on_buildd.canChangeExpirationDate(foobar)
     True
-    >>> one_day_ago = datetime.now(pytz.timezone("UTC")) - timedelta(days=1)
-    >>> tomorrow = datetime.now(pytz.timezone("UTC")) + timedelta(days=1)
+    >>> one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
+    >>> tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
     >>> foobar_on_buildd.setExpirationDate(one_day_ago, foobar)
     Traceback (most recent call last):
     ...
@@ -746,7 +745,7 @@ still active.
     >>> karl_on_mirroradmins = membershipset.getByPersonAndTeam(
     ...     karl, mirror_admins
     ... )
-    >>> tomorrow = datetime.now(pytz.timezone("UTC")) + timedelta(days=1)
+    >>> tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
     >>> print(karl_on_mirroradmins.status.title)
     Approved
     >>> print(karl_on_mirroradmins.dateexpires)
diff --git a/lib/lp/registry/interfaces/poll.py b/lib/lp/registry/interfaces/poll.py
index fee8d8b..9da29b1 100644
--- a/lib/lp/registry/interfaces/poll.py
+++ b/lib/lp/registry/interfaces/poll.py
@@ -19,9 +19,8 @@ __all__ = [
 ]
 
 import http.client
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from lazr.enum import DBEnumeratedType, DBItem
 from lazr.restful.declarations import (
     collection_default_content,
@@ -233,11 +232,11 @@ class IPoll(Interface):
             raise Invalid(
                 "A poll cannot close at the time (or before) it opens."
             )
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         # Allow a bit of slack to account for time between form creation and
         # validation.
         twelve_hours_ahead = now + timedelta(hours=12) - timedelta(seconds=60)
-        start_date = poll.dateopens.astimezone(pytz.UTC)
+        start_date = poll.dateopens.astimezone(timezone.utc)
         if start_date < twelve_hours_ahead:
             raise Invalid(
                 "A poll cannot open less than 12 hours after it's created."
diff --git a/lib/lp/registry/mail/teammembership.py b/lib/lp/registry/mail/teammembership.py
index 45d1410..f1c7d9d 100644
--- a/lib/lp/registry/mail/teammembership.py
+++ b/lib/lp/registry/mail/teammembership.py
@@ -6,9 +6,8 @@ __all__ = [
 ]
 
 from collections import OrderedDict
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.component import getUtility
 
 from lp.app.browser.tales import DurationFormatterAPI
@@ -391,7 +390,9 @@ class TeamMembershipMailer(BaseMailer):
                 member, team, recipient
             )
 
-        formatter = DurationFormatterAPI(dateexpires - datetime.now(pytz.UTC))
+        formatter = DurationFormatterAPI(
+            dateexpires - datetime.now(timezone.utc)
+        )
         extra_params = {
             "how_to_renew": how_to_renew,
             "expiration_date": dateexpires.strftime("%Y-%m-%d"),
diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
index dd47d69..ffcc794 100644
--- a/lib/lp/registry/model/accesspolicy.py
+++ b/lib/lp/registry/model/accesspolicy.py
@@ -14,9 +14,9 @@ __all__ = [
 ]
 
 from collections import defaultdict
+from datetime import timezone
 from itertools import product
 
-import pytz
 from storm.expr import SQL, And, In, Or, Select
 from storm.properties import DateTime, Int
 from storm.references import Reference
@@ -396,7 +396,7 @@ class AccessArtifactGrant(StormBase):
     grantee = Reference(grantee_id, "Person.id")
     grantor_id = Int(name="grantor")
     grantor = Reference(grantor_id, "Person.id")
-    date_created = DateTime(tzinfo=pytz.UTC)
+    date_created = DateTime(tzinfo=timezone.utc)
 
     @property
     def concrete_artifact(self):
@@ -456,7 +456,7 @@ class AccessPolicyGrant(StormBase):
     grantee = Reference(grantee_id, "Person.id")
     grantor_id = Int(name="grantor")
     grantor = Reference(grantor_id, "Person.id")
-    date_created = DateTime(tzinfo=pytz.UTC)
+    date_created = DateTime(tzinfo=timezone.utc)
 
     @classmethod
     def grant(cls, grants):
diff --git a/lib/lp/registry/model/announcement.py b/lib/lp/registry/model/announcement.py
index 82d4be6..526d4ab 100644
--- a/lib/lp/registry/model/announcement.py
+++ b/lib/lp/registry/model/announcement.py
@@ -10,7 +10,8 @@ __all__ = [
     "MakesAnnouncements",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.expr import And, LeftJoin, Or, Select
 from storm.properties import Bool, DateTime, Int, Unicode
 from storm.references import Reference
@@ -39,10 +40,14 @@ class Announcement(StormBase):
 
     id = Int(primary=True)
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
-    date_announced = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
+    date_announced = DateTime(
+        allow_none=True, default=None, tzinfo=timezone.utc
+    )
     date_last_modified = DateTime(
-        name="date_updated", allow_none=True, default=None, tzinfo=pytz.UTC
+        name="date_updated", allow_none=True, default=None, tzinfo=timezone.utc
     )
 
     registrant_id = Int(
diff --git a/lib/lp/registry/model/codeofconduct.py b/lib/lp/registry/model/codeofconduct.py
index 0ed0369..1791a14 100644
--- a/lib/lp/registry/model/codeofconduct.py
+++ b/lib/lp/registry/model/codeofconduct.py
@@ -15,9 +15,8 @@ __all__ = [
 ]
 
 import os
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import six
 from storm.locals import Bool, DateTime, Int, Not, Reference, Unicode
 from zope.component import getUtility
@@ -64,7 +63,7 @@ class CodeOfConductConf:
     # Set the datereleased to the date that 1.0 CoC was released,
     # preserving everyone's Ubuntu Code of Conduct signatory status.
     # https://launchpad.net/products/launchpad/+bug/48995
-    datereleased = datetime(2005, 4, 12, tzinfo=pytz.timezone("UTC"))
+    datereleased = datetime(2005, 4, 12, tzinfo=timezone.utc)
 
 
 @implementer(ICodeOfConduct)
@@ -184,7 +183,10 @@ class SignedCodeOfConduct(StormBase):
     signing_key_fingerprint = Unicode()
 
     datecreated = DateTime(
-        tzinfo=pytz.UTC, name="datecreated", allow_none=False, default=UTC_NOW
+        tzinfo=timezone.utc,
+        name="datecreated",
+        allow_none=False,
+        default=UTC_NOW,
     )
 
     recipient_id = Int(name="recipient", allow_none=True, default=None)
diff --git a/lib/lp/registry/model/commercialsubscription.py b/lib/lp/registry/model/commercialsubscription.py
index cdd16c5..0d7af01 100644
--- a/lib/lp/registry/model/commercialsubscription.py
+++ b/lib/lp/registry/model/commercialsubscription.py
@@ -5,9 +5,8 @@
 
 __all__ = ["CommercialSubscription"]
 
-import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.locals import DateTime, Int, Reference, Store, Unicode
 from zope.interface import implementer
 
@@ -35,12 +34,18 @@ class CommercialSubscription(StormBase):
     distribution_id = Int(name="distribution", allow_none=True)
     distribution = Reference(distribution_id, "Distribution.id")
 
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
     date_last_modified = DateTime(
-        tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    date_starts = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    date_expires = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
     )
-    date_starts = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
-    date_expires = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
 
     registrant_id = Int(
         name="registrant", allow_none=False, validator=validate_public_person
@@ -90,7 +95,7 @@ class CommercialSubscription(StormBase):
     @property
     def is_active(self):
         """See `ICommercialSubscription`"""
-        now = datetime.datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         return self.date_starts < now < self.date_expires
 
     def delete(self):
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 1a46efd..36f3543 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -10,10 +10,9 @@ __all__ = [
 
 import itertools
 from collections import defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import itemgetter
 
-import pytz
 from storm.expr import (
     SQL,
     And,
@@ -561,7 +560,7 @@ class Distribution(
 
     @property
     def has_current_commercial_subscription(self):
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         return (
             self.commercial_subscription
             and self.commercial_subscription.date_expires > now
@@ -585,7 +584,7 @@ class Distribution(
             warning_date = (
                 self.commercial_subscription.date_expires - timedelta(30)
             )
-            now = datetime.now(pytz.UTC)
+            now = datetime.now(timezone.utc)
             if now > warning_date:
                 # The subscription is close to being expired.
                 return True
@@ -597,7 +596,7 @@ class Distribution(
         """Create a complementary commercial subscription for the distro."""
         if not self.commercial_subscription:
             lp_janitor = getUtility(ILaunchpadCelebrities).janitor
-            now = datetime.now(pytz.UTC)
+            now = datetime.now(timezone.utc)
             date_expires = now + timedelta(days=30)
             sales_system_id = "complimentary-30-day-%s" % now
             whiteboard = (
diff --git a/lib/lp/registry/model/distributionmirror.py b/lib/lp/registry/model/distributionmirror.py
index 7e350cf..7e5ec72 100644
--- a/lib/lp/registry/model/distributionmirror.py
+++ b/lib/lp/registry/model/distributionmirror.py
@@ -12,9 +12,8 @@ __all__ = [
     "MirrorCDImageDistroSeries",
 ]
 
-from datetime import MINYEAR, datetime, timedelta
+from datetime import MINYEAR, datetime, timedelta, timezone
 
-import pytz
 from storm.expr import Cast, Coalesce, LeftJoin
 from storm.locals import (
     And,
@@ -116,8 +115,10 @@ class DistributionMirror(StormBase):
         default=MirrorStatus.PENDING_REVIEW,
         enum=MirrorStatus,
     )
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
-    date_reviewed = DateTime(tzinfo=pytz.UTC, default=None)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    date_reviewed = DateTime(tzinfo=timezone.utc, default=None)
     whiteboard = Unicode(allow_none=True, default=None)
     country_dns_mirror = Bool(allow_none=False, default=False)
 
@@ -845,9 +846,9 @@ class _MirrorSeriesMixIn:
     def getURLsToCheckUpdateness(self, when=None):
         """See IMirrorDistroSeriesSource or IMirrorDistroArchSeries."""
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
 
-        start = datetime(MINYEAR, 1, 1, tzinfo=pytz.timezone("UTC"))
+        start = datetime(MINYEAR, 1, 1, tzinfo=timezone.utc)
         time_interval = (start, when)
         latest_upload = self.getLatestPublishingEntry(time_interval)
         if latest_upload is None:
@@ -1123,7 +1124,9 @@ class MirrorProbeRecord(StormBase):
     )
     log_file_id = Int(name="log_file", allow_none=False)
     log_file = Reference(log_file_id, "LibraryFileAlias.id")
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     def __init__(self, distribution_mirror, log_file):
         self.distribution_mirror = distribution_mirror
diff --git a/lib/lp/registry/model/karma.py b/lib/lp/registry/model/karma.py
index b533689..5a4d44f 100644
--- a/lib/lp/registry/model/karma.py
+++ b/lib/lp/registry/model/karma.py
@@ -13,7 +13,8 @@ __all__ = [
     "KarmaContextMixin",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Desc, Int, Reference, ReferenceSet, Unicode
 from zope.interface import implementer
 
@@ -66,7 +67,10 @@ class Karma(StormBase):
     sourcepackagename_id = Int(name="sourcepackagename", allow_none=True)
     sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")
     datecreated = DateTime(
-        name="datecreated", allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC
+        name="datecreated",
+        allow_none=False,
+        default=UTC_NOW,
+        tzinfo=timezone.utc,
     )
 
     def __init__(
diff --git a/lib/lp/registry/model/mailinglist.py b/lib/lp/registry/model/mailinglist.py
index 9c9831a..542ff3c 100644
--- a/lib/lp/registry/model/mailinglist.py
+++ b/lib/lp/registry/model/mailinglist.py
@@ -13,10 +13,10 @@ __all__ = [
 
 import collections
 import operator
+from datetime import timezone
 from socket import getfqdn
 from string import Template
 
-import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from storm.expr import Func
 from storm.info import ClassAlias
@@ -111,7 +111,9 @@ class MessageApproval(StormBase):
     posted_message_id = Int(name="posted_message", allow_none=False)
     posted_message = Reference(posted_message_id, "LibraryFileAlias.id")
 
-    posted_date = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    posted_date = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     mailing_list_id = Int(name="mailing_list", allow_none=False)
     mailing_list = Reference(mailing_list_id, "MailingList.id")
@@ -127,7 +129,7 @@ class MessageApproval(StormBase):
     )
     disposed_by = Reference(disposed_by_id, "Person.id")
 
-    disposal_date = DateTime(tzinfo=pytz.UTC, default=None)
+    disposal_date = DateTime(tzinfo=timezone.utc, default=None)
 
     def __init__(
         self, message, posted_by, posted_message, posted_date, mailing_list
@@ -200,7 +202,7 @@ class MailingList(StormBase):
     registrant = Reference(registrant_id, "Person.id")
 
     date_registered = DateTime(
-        tzinfo=pytz.UTC, allow_none=False, default=DEFAULT
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
     )
 
     reviewer_id = Int(
@@ -208,9 +210,13 @@ class MailingList(StormBase):
     )
     reviewer = Reference(reviewer_id, "Person.id")
 
-    date_reviewed = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_reviewed = DateTime(
+        tzinfo=timezone.utc, allow_none=True, default=None
+    )
 
-    date_activated = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_activated = DateTime(
+        tzinfo=timezone.utc, allow_none=True, default=None
+    )
 
     status = DBEnum(
         enum=MailingListStatus,
@@ -835,7 +841,9 @@ class MailingListSubscription(StormBase):
     mailing_list_id = Int(name="mailing_list", allow_none=False)
     mailing_list = Reference(mailing_list_id, "MailingList.id")
 
-    date_joined = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_joined = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
 
     email_address_id = Int(name="email_address")
     email_address = Reference(email_address_id, "EmailAddress.id")
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index a686b17..e946258 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -9,8 +9,8 @@ __all__ = [
 ]
 
 from collections import defaultdict
+from datetime import timezone
 
-import pytz
 from storm.expr import Join, LeftJoin, Or
 from storm.locals import Bool, DateTime, Int, Reference, Unicode
 from zope.component import getUtility
@@ -84,10 +84,10 @@ class OCIProject(BugTargetBase, StructuralSubscriptionTargetMixin, StormBase):
 
     id = Int(primary=True)
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+        name="date_last_modified", tzinfo=timezone.utc, allow_none=False
     )
 
     registrant_id = Int(name="registrant", allow_none=False)
diff --git a/lib/lp/registry/model/ociprojectseries.py b/lib/lp/registry/model/ociprojectseries.py
index 092f8a2..378932a 100644
--- a/lib/lp/registry/model/ociprojectseries.py
+++ b/lib/lp/registry/model/ociprojectseries.py
@@ -7,7 +7,8 @@ __all__ = [
     "OCIProjectSeries",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
 
@@ -37,7 +38,7 @@ class OCIProjectSeries(StormBase):
     summary = Unicode(name="summary", allow_none=False)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
 
     registrant_id = Int(name="registrant", allow_none=False)
diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
index ab52ad0..8bf791f 100644
--- a/lib/lp/registry/model/person.py
+++ b/lib/lp/registry/model/person.py
@@ -32,10 +32,9 @@ import copy
 import random
 import re
 import weakref
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-import pytz
 import six
 import transaction
 from lazr.delegates import delegate_to
@@ -1285,7 +1284,8 @@ class Person(
             )
             .find(
                 Person,
-                CommercialSubscription.date_expires > datetime.now(pytz.UTC),
+                CommercialSubscription.date_expires
+                > datetime.now(timezone.utc),
                 Person.id == self.id,
             )
         )
@@ -2983,7 +2983,7 @@ class Person(
         """See `IPerson`."""
         days = self.defaultmembershipperiod
         if days:
-            return datetime.now(pytz.timezone("UTC")) + timedelta(days)
+            return datetime.now(timezone.utc) + timedelta(days)
         else:
             return None
 
@@ -2992,7 +2992,7 @@ class Person(
         """See `IPerson`."""
         days = self.defaultrenewalperiod
         if days:
-            return datetime.now(pytz.timezone("UTC")) + timedelta(days)
+            return datetime.now(timezone.utc) + timedelta(days)
         else:
             return None
 
diff --git a/lib/lp/registry/model/personlocation.py b/lib/lp/registry/model/personlocation.py
index 602921a..22bba6f 100644
--- a/lib/lp/registry/model/personlocation.py
+++ b/lib/lp/registry/model/personlocation.py
@@ -13,7 +13,8 @@ __all__ = [
     "PersonLocation",
 ]
 
-import pytz
+from datetime import timezone
+
 import six
 from storm.locals import Bool, DateTime, Float, Int, Reference, Unicode
 from zope.interface import implementer
@@ -34,7 +35,10 @@ class PersonLocation(StormBase):
     id = Int(primary=True)
 
     date_created = DateTime(
-        tzinfo=pytz.UTC, name="date_created", allow_none=False, default=UTC_NOW
+        tzinfo=timezone.utc,
+        name="date_created",
+        allow_none=False,
+        default=UTC_NOW,
     )
 
     person_id = Int(name="person", allow_none=False)
@@ -52,7 +56,7 @@ class PersonLocation(StormBase):
     last_modified_by = Reference(last_modified_by_id, "Person.id")
 
     date_last_modified = DateTime(
-        tzinfo=pytz.UTC,
+        tzinfo=timezone.utc,
         name="date_last_modified",
         allow_none=False,
         default=UTC_NOW,
diff --git a/lib/lp/registry/model/personnotification.py b/lib/lp/registry/model/personnotification.py
index d420c93..02254ae 100644
--- a/lib/lp/registry/model/personnotification.py
+++ b/lib/lp/registry/model/personnotification.py
@@ -8,9 +8,8 @@ __all__ = [
     "PersonNotificationSet",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.locals import DateTime, Int, Unicode
 from storm.references import Reference
 from storm.store import Store
@@ -38,10 +37,13 @@ class PersonNotification(StormBase):
     person = Reference(person_id, "Person.id")
 
     date_created = DateTime(
-        tzinfo=pytz.UTC, name="date_created", allow_none=False, default=UTC_NOW
+        tzinfo=timezone.utc,
+        name="date_created",
+        allow_none=False,
+        default=UTC_NOW,
     )
     date_emailed = DateTime(
-        tzinfo=pytz.UTC, name="date_emailed", allow_none=True
+        tzinfo=timezone.utc, name="date_emailed", allow_none=True
     )
 
     body = Unicode(name="body", allow_none=False)
@@ -82,7 +84,7 @@ class PersonNotification(StormBase):
             logger.info("Sending notification to %r." % to_addresses)
         from_addr = config.canonical.bounce_address
         simple_sendmail(from_addr, to_addresses, self.subject, self.body)
-        self.date_emailed = datetime.now(pytz.timezone("UTC"))
+        self.date_emailed = datetime.now(timezone.utc)
 
     def destroySelf(self):
         """See `IPersonNotification`."""
diff --git a/lib/lp/registry/model/persontransferjob.py b/lib/lp/registry/model/persontransferjob.py
index bf6bb53..292f256 100644
--- a/lib/lp/registry/model/persontransferjob.py
+++ b/lib/lp/registry/model/persontransferjob.py
@@ -10,9 +10,8 @@ __all__ = [
 ]
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import six
 import transaction
 from lazr.delegates import delegate_to
@@ -184,7 +183,7 @@ class PersonTransferJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
     @classmethod
     def _deserialiseDateTime(cls, dt_str):
         dt = datetime.strptime(dt_str, cls._time_format)
-        return dt.replace(tzinfo=pytz.UTC)
+        return dt.replace(tzinfo=timezone.utc)
 
 
 @implementer(IMembershipNotificationJob)
diff --git a/lib/lp/registry/model/poll.py b/lib/lp/registry/model/poll.py
index 6f23f14..fff8cb1 100644
--- a/lib/lp/registry/model/poll.py
+++ b/lib/lp/registry/model/poll.py
@@ -12,9 +12,8 @@ __all__ = [
     "VoteCastSet",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from storm.locals import (
     And,
     Bool,
@@ -74,9 +73,13 @@ class Poll(StormBase):
 
     title = Unicode(name="title", allow_none=False)
 
-    dateopens = DateTime(tzinfo=pytz.UTC, name="dateopens", allow_none=False)
+    dateopens = DateTime(
+        tzinfo=timezone.utc, name="dateopens", allow_none=False
+    )
 
-    datecloses = DateTime(tzinfo=pytz.UTC, name="datecloses", allow_none=False)
+    datecloses = DateTime(
+        tzinfo=timezone.utc, name="datecloses", allow_none=False
+    )
 
     proposition = Unicode(name="proposition", allow_none=False)
 
@@ -120,29 +123,29 @@ class Poll(StormBase):
     def isOpen(self, when=None):
         """See IPoll."""
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
         return self.datecloses >= when and self.dateopens <= when
 
     @property
     def closesIn(self):
         """See IPoll."""
-        return self.datecloses - datetime.now(pytz.timezone("UTC"))
+        return self.datecloses - datetime.now(timezone.utc)
 
     @property
     def opensIn(self):
         """See IPoll."""
-        return self.dateopens - datetime.now(pytz.timezone("UTC"))
+        return self.dateopens - datetime.now(timezone.utc)
 
     def isClosed(self, when=None):
         """See IPoll."""
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
         return self.datecloses <= when
 
     def isNotYetOpened(self, when=None):
         """See IPoll."""
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
         return self.dateopens > when
 
     def getAllOptions(self):
@@ -369,7 +372,7 @@ class PollSet:
         if status is None:
             status = PollStatus.ALL
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
 
         status = set(status)
         status_clauses = []
diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
index f7c3b99..5ce459e 100644
--- a/lib/lp/registry/model/product.py
+++ b/lib/lp/registry/model/product.py
@@ -11,11 +11,10 @@ __all__ = [
 ]
 
 
-import datetime
 import http.client
 import operator
+from datetime import datetime, time, timedelta, timezone
 
-import pytz
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.restful.declarations import error_status
 from storm.expr import (
@@ -768,7 +767,7 @@ class Product(
 
     @property
     def has_current_commercial_subscription(self):
-        now = datetime.datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         return (
             self.commercial_subscription
             and self.commercial_subscription.date_expires > now
@@ -815,10 +814,9 @@ class Product(
             return True
         else:
             warning_date = (
-                self.commercial_subscription.date_expires
-                - datetime.timedelta(30)
+                self.commercial_subscription.date_expires - timedelta(30)
             )
-            now = datetime.datetime.now(pytz.timezone("UTC"))
+            now = datetime.now(timezone.utc)
             if now > warning_date:
                 # The subscription is close to being expired.
                 return True
@@ -960,8 +958,8 @@ class Product(
         """Create a complementary commercial subscription for the product"""
         if not self.commercial_subscription:
             lp_janitor = getUtility(ILaunchpadCelebrities).janitor
-            now = datetime.datetime.now(pytz.UTC)
-            date_expires = now + datetime.timedelta(days=30)
+            now = datetime.now(timezone.utc)
+            date_expires = now + timedelta(days=30)
             sales_system_id = "complimentary-30-day-%s" % now
             whiteboard = (
                 "Complimentary 30 day subscription. -- Launchpad %s"
@@ -2048,29 +2046,27 @@ class ProductSet:
             The returned time will have a zero time component and be based on
             UTC.
             """
-            return datetime.datetime.combine(
-                date, datetime.time(tzinfo=pytz.UTC)
-            )
+            return datetime.combine(date, time(tzinfo=timezone.utc))
 
         if created_after is not None:
-            if not isinstance(created_after, datetime.datetime):
+            if not isinstance(created_after, datetime):
                 created_after = dateToDatetime(created_after)
-                created_after = datetime.datetime(
+                created_after = datetime(
                     created_after.year,
                     created_after.month,
                     created_after.day,
-                    tzinfo=pytz.utc,
+                    tzinfo=timezone.utc,
                 )
             conditions.append(Product.datecreated >= created_after)
 
         if created_before is not None:
-            if not isinstance(created_before, datetime.datetime):
+            if not isinstance(created_before, datetime):
                 created_before = dateToDatetime(created_before)
             conditions.append(Product.datecreated <= created_before)
 
         subscription_conditions = []
         if subscription_expires_after is not None:
-            if not isinstance(subscription_expires_after, datetime.datetime):
+            if not isinstance(subscription_expires_after, datetime):
                 subscription_expires_after = dateToDatetime(
                     subscription_expires_after
                 )
@@ -2080,7 +2076,7 @@ class ProductSet:
             )
 
         if subscription_expires_before is not None:
-            if not isinstance(subscription_expires_before, datetime.datetime):
+            if not isinstance(subscription_expires_before, datetime):
                 subscription_expires_before = dateToDatetime(
                     subscription_expires_before
                 )
@@ -2090,7 +2086,7 @@ class ProductSet:
             )
 
         if subscription_modified_after is not None:
-            if not isinstance(subscription_modified_after, datetime.datetime):
+            if not isinstance(subscription_modified_after, datetime):
                 subscription_modified_after = dateToDatetime(
                     subscription_modified_after
                 )
@@ -2099,7 +2095,7 @@ class ProductSet:
                 >= subscription_modified_after
             )
         if subscription_modified_before is not None:
-            if not isinstance(subscription_modified_before, datetime.datetime):
+            if not isinstance(subscription_modified_before, datetime):
                 subscription_modified_before = dateToDatetime(
                     subscription_modified_before
                 )
diff --git a/lib/lp/registry/model/productjob.py b/lib/lp/registry/model/productjob.py
index 2b45425..a42096d 100644
--- a/lib/lp/registry/model/productjob.py
+++ b/lib/lp/registry/model/productjob.py
@@ -12,11 +12,10 @@ __all__ = [
 ]
 
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import six
 from lazr.delegates import delegate_to
-from pytz import utc
 from storm.expr import And, Not, Select
 from storm.locals import Int, Reference, Unicode
 from zope.component import getUtility
@@ -426,7 +425,7 @@ class SevenDayCommercialExpirationJob(
 
     @staticmethod
     def _get_expiration_dates():
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         in_seven_days = now + timedelta(days=7)
         seven_days_ago = now - timedelta(days=7)
         return now, in_seven_days, seven_days_ago
@@ -443,7 +442,7 @@ class ThirtyDayCommercialExpirationJob(
 
     @staticmethod
     def _get_expiration_dates():
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         # Avoid overlay with the seven day notification.
         in_twenty_three_days = now + timedelta(days=7)
         in_thirty_days = now + timedelta(days=30)
@@ -465,7 +464,7 @@ class CommercialExpiredJob(CommericialExpirationMixin, ProductNotificationJob):
 
     @staticmethod
     def _get_expiration_dates():
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         ten_years_ago = now - timedelta(days=3650)
         thirty_days_ago = now - timedelta(days=30)
         return ten_years_ago, now, thirty_days_ago
diff --git a/lib/lp/registry/model/teammembership.py b/lib/lp/registry/model/teammembership.py
index 891b057..dbf088d 100644
--- a/lib/lp/registry/model/teammembership.py
+++ b/lib/lp/registry/model/teammembership.py
@@ -8,9 +8,8 @@ __all__ = [
     "TeamParticipation",
 ]
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.expr import Func
 from storm.info import ClassAlias
 from storm.store import Store
@@ -124,7 +123,7 @@ class TeamMembership(SQLBase):
         # cover the fencepost error when `date_limit` is
         # earlier than `self.dateexpires`, which happens later
         # in the same day.
-        date_limit = datetime.now(pytz.UTC) + timedelta(
+        date_limit = datetime.now(timezone.utc) + timedelta(
             days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT + 1
         )
         return (
@@ -164,8 +163,9 @@ class TeamMembership(SQLBase):
         self._setExpirationDate(date, user)
 
     def _setExpirationDate(self, date, user):
-        UTC = pytz.timezone("UTC")
-        assert date is None or date.date() >= datetime.now(UTC).date(), (
+        assert (
+            date is None or date.date() >= datetime.now(timezone.utc).date()
+        ), (
             "The given expiration date must be None or be in the future: %s"
             % date.strftime("%Y-%m-%d")
         )
@@ -179,7 +179,7 @@ class TeamMembership(SQLBase):
                 "%s in team %s has no membership expiration date."
                 % (self.person.name, self.team.name)
             )
-        if self.dateexpires < datetime.now(pytz.timezone("UTC")):
+        if self.dateexpires < datetime.now(timezone.utc):
             # The membership has reached expiration. Silently return because
             # there is nothing to do. The member will have received emails
             # from previous calls by flag-expired-memberships.py
@@ -243,7 +243,7 @@ class TeamMembership(SQLBase):
         old_status = self.status
         self.status = status
 
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         if status in [proposed, invited]:
             self.proposed_by = user
             self.proponent_comment = comment
@@ -337,7 +337,7 @@ class TeamMembershipSet:
             person=person, team=team, status=status, dateexpires=dateexpires
         )
 
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         tm.proposed_by = user
         tm.date_proposed = now
         tm.proponent_comment = comment
@@ -368,7 +368,7 @@ class TeamMembershipSet:
     def getMembershipsToExpire(self, when=None):
         """See `ITeamMembershipSet`."""
         if when is None:
-            when = datetime.now(pytz.timezone("UTC"))
+            when = datetime.now(timezone.utc)
         conditions = [
             TeamMembership.dateexpires <= when,
             TeamMembership.status.is_in(
@@ -379,7 +379,7 @@ class TeamMembershipSet:
 
     def getExpiringMembershipsToWarn(self):
         """See `ITeamMembershipSet`,"""
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         min_date_for_daily_warning = now + timedelta(days=7)
         memberships_to_warn = set(
             self.getMembershipsToExpire(min_date_for_daily_warning)
@@ -407,7 +407,7 @@ class TeamMembershipSet:
 
     def deactivateActiveMemberships(self, team, comment, reviewer):
         """See `ITeamMembershipSet`."""
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         cur = cursor()
         all_members = list(team.activemembers)
         cur.execute(
diff --git a/lib/lp/registry/scripts/personnotification.py b/lib/lp/registry/scripts/personnotification.py
index b8cfe69..fe4941b 100644
--- a/lib/lp/registry/scripts/personnotification.py
+++ b/lib/lp/registry/scripts/personnotification.py
@@ -8,9 +8,8 @@ __all__ = [
 ]
 
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.store import Store
 from zope.component import getUtility
 
@@ -63,7 +62,7 @@ class PersonNotificationManager:
         retained_days = timedelta(
             days=int(config.person_notification.retained_days)
         )
-        time_limit = datetime.now(pytz.timezone("UTC")) - retained_days
+        time_limit = datetime.now(timezone.utc) - retained_days
         notification_set = getUtility(IPersonNotificationSet)
         to_delete = notification_set.getNotificationsOlderThan(time_limit)
         if to_delete.count():
diff --git a/lib/lp/registry/scripts/productreleasefinder/finder.py b/lib/lp/registry/scripts/productreleasefinder/finder.py
index e810a48..6126fb0 100644
--- a/lib/lp/registry/scripts/productreleasefinder/finder.py
+++ b/lib/lp/registry/scripts/productreleasefinder/finder.py
@@ -8,10 +8,9 @@ import os
 import re
 import tempfile
 from collections import defaultdict
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import urlsplit
 
-import pytz
 import requests
 from zope.component import getUtility
 
@@ -205,7 +204,7 @@ class ProductReleaseFinder:
         release = milestone.product_release
         if release is None:
             release = milestone.createProductRelease(
-                owner=product.owner, datereleased=datetime.now(pytz.UTC)
+                owner=product.owner, datereleased=datetime.now(timezone.utc)
             )
             self.log.info(
                 "Created new release %s for %s/%s",
diff --git a/lib/lp/registry/scripts/tests/test_closeaccount.py b/lib/lp/registry/scripts/tests/test_closeaccount.py
index 8e2292b..e9cd611 100644
--- a/lib/lp/registry/scripts/tests/test_closeaccount.py
+++ b/lib/lp/registry/scripts/tests/test_closeaccount.py
@@ -2,9 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test the close-account script."""
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from storm.store import Store
 from testtools.matchers import (
@@ -1168,7 +1167,7 @@ class TestCloseAccount(TestCaseWithFactory):
             account_id = person.account.id
 
             milestone = self.factory.makeMilestone(**milestone_target)
-            milestone.createProductRelease(person, datetime.now(pytz.UTC))
+            milestone.createProductRelease(person, datetime.now(timezone.utc))
             script = self.makeScript([person.name])
             with dbuser("launchpad"):
                 if not expected_to_be_removed:
@@ -1208,7 +1207,7 @@ class TestCloseAccount(TestCaseWithFactory):
 
             milestone = self.factory.makeMilestone(**milestone_target)
             product_release = milestone.createProductRelease(
-                milestone.product.owner, datetime.now(pytz.UTC)
+                milestone.product.owner, datetime.now(timezone.utc)
             )  # type: ProductRelease
             product_release.addReleaseFile(
                 "test.txt", b"test", "text/plain", person
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.rst b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.rst
index e4e17d7..ea5b4eb 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.rst
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.rst
@@ -219,13 +219,14 @@ token to a fixed value:
     ...     1
     ... ].encode("ASCII")
 
-    >>> import datetime, hashlib, pytz
+    >>> from datetime import datetime, timezone
+    >>> import hashlib
     >>> from lp.services.verification.model.logintoken import LoginToken
     >>> logintoken = LoginToken.selectOneBy(
     ...     _token=hashlib.sha256(token_value).hexdigest()
     ... )
-    >>> logintoken.date_created = datetime.datetime(
-    ...     2005, 4, 1, 12, 0, 0, tzinfo=pytz.timezone("UTC")
+    >>> logintoken.date_created = datetime(
+    ...     2005, 4, 1, 12, 0, 0, tzinfo=timezone.utc
     ... )
     >>> logintoken.sync()
 
diff --git a/lib/lp/registry/stories/pillar/xx-pillar-sprints.rst b/lib/lp/registry/stories/pillar/xx-pillar-sprints.rst
index e2173eb..2364bc9 100644
--- a/lib/lp/registry/stories/pillar/xx-pillar-sprints.rst
+++ b/lib/lp/registry/stories/pillar/xx-pillar-sprints.rst
@@ -4,12 +4,8 @@ Sprints relevant for pillars
 For Products, ProjectGroups and Distributions, we have a +sprints page which
 lists all events relevant to that pillar.
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
+    >>> from datetime import datetime, timedelta, timezone
     >>> import re
-    >>> import pytz
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.product import IProductSet
@@ -25,7 +21,7 @@ lists all events relevant to that pillar.
     >>> futurista = factory.makeSprint(
     ...     name="futurista",
     ...     title="Future Mega Meeting",
-    ...     time_starts=datetime.now(pytz.UTC) + timedelta(days=1),
+    ...     time_starts=datetime.now(timezone.utc) + timedelta(days=1),
     ... )
     >>> firefox = getUtility(IProductSet).getByName("firefox")
     >>> firefox_spec = firefox.specifications(futurista.owner)[0]
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-view.rst b/lib/lp/registry/stories/productrelease/xx-productrelease-view.rst
index 0068312..867b8aa 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-view.rst
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-view.rst
@@ -40,7 +40,7 @@ downloaded and the date of the last download on that table as well.
 
     # Manually update the download counter for that file above so that we can
     # test it.
-    >>> from datetime import date, datetime
+    >>> from datetime import date, datetime, timezone
     >>> from lp.services.librarian.model import LibraryFileAlias
     >>> lfa = LibraryFileAlias.selectOne(
     ...     LibraryFileAlias.q.filename == "firefox_0.9.2.orig.tar.gz"
@@ -59,8 +59,7 @@ When a file has been downloaded on the present day, all we can say is that
 it's been downloaded "today".  That's because we don't have the time it was
 downloaded, so we can't say it was downloaded a few minutes/hours ago.
 
-    >>> import pytz
-    >>> lfa.updateDownloadCount(datetime.now(pytz.utc).date(), None, 4356)
+    >>> lfa.updateDownloadCount(datetime.now(timezone.utc).date(), None, 4356)
     >>> anon_browser.reload()
     >>> print(
     ...     extract_text(find_tag_by_id(anon_browser.contents, "downloads"))
diff --git a/lib/lp/registry/stories/team-polls/create-polls.rst b/lib/lp/registry/stories/team-polls/create-polls.rst
index e79d5ab..d12afbb 100644
--- a/lib/lp/registry/stories/team-polls/create-polls.rst
+++ b/lib/lp/registry/stories/team-polls/create-polls.rst
@@ -100,15 +100,13 @@ We're redirected to the newly created poll page.
 
 Create a new poll that starts tomorrow and will last for ten years.
 
-    >>> from datetime import (
-    ...     datetime,
-    ...     timedelta,
-    ... )
-    >>> import pytz
+    >>> from datetime import datetime, timedelta, timezone
 
-    >>> tomorrow = (datetime.now(pytz.UTC) + timedelta(days=1)).isoformat()
+    >>> tomorrow = (
+    ...     datetime.now(timezone.utc) + timedelta(days=1)
+    ... ).isoformat()
     >>> ten_years_from_now = (
-    ...     datetime.now(pytz.UTC) + timedelta(days=3560)
+    ...     datetime.now(timezone.utc) + timedelta(days=3560)
     ... ).isoformat()
     >>> team_admin_browser.open("http://launchpad.test/~ubuntu-team/+newpoll";)
     >>> team_admin_browser.getControl(
diff --git a/lib/lp/registry/stories/team/xx-team-membership.rst b/lib/lp/registry/stories/team/xx-team-membership.rst
index 68aede4..90c2684 100644
--- a/lib/lp/registry/stories/team/xx-team-membership.rst
+++ b/lib/lp/registry/stories/team/xx-team-membership.rst
@@ -76,7 +76,7 @@ Give up on change, nothing should have changed with Colin:
 Now revoke Colin's administrator status and make him expire in November
 next year -- successfully.
 
-    >>> from datetime import datetime, timedelta
+    >>> from datetime import datetime, timedelta, timezone
     >>> expire_date = datetime.utcnow() + timedelta(days=365)
 
     >>> browser.getControl(name="admin").value = ["no"]
@@ -234,7 +234,6 @@ Sample Person has both direct and indirect memberships, and expiry dates are
 shown where relevant.  We force an expiration date to be in the past in
 order to avoid this test failing at some point in the future.
 
-    >>> import pytz
     >>> from zope.security.proxy import removeSecurityProxy
 
     >>> login("admin@xxxxxxxxxxxxx")
@@ -242,7 +241,7 @@ order to avoid this test failing at some point in the future.
     >>> lp_users = personset.getByName("launchpad-users")
     >>> membership = teammembershipset.getByPersonAndTeam(name12, lp_users)
     >>> removeSecurityProxy(membership).dateexpires = datetime(
-    ...     2009, 1, 1, tzinfo=pytz.UTC
+    ...     2009, 1, 1, tzinfo=timezone.utc
     ... )
     >>> logout()
 
diff --git a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.rst b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.rst
index 81a9a34..a158283 100644
--- a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.rst
+++ b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.rst
@@ -92,9 +92,8 @@ able to renew it himself.
 See lib/lp/registry/stories/team/xx-team-membership.rst for an explanation
 of the expiry._control.attrs TestBrowser voodoo.
 
-    >>> from datetime import datetime, timedelta
-    >>> import pytz
-    >>> now = datetime.now(pytz.timezone("UTC"))
+    >>> from datetime import datetime, timedelta, timezone
+    >>> now = datetime.now(timezone.utc)
     >>> day_after_tomorrow = now + timedelta(days=2)
 
     >>> team_admin_browser = setupBrowser(auth="Basic mark@xxxxxxxxxxx:test")
diff --git a/lib/lp/registry/stories/webservice/xx-person.rst b/lib/lp/registry/stories/webservice/xx-person.rst
index 1249d3e..f9e73af 100644
--- a/lib/lp/registry/stories/webservice/xx-person.rst
+++ b/lib/lp/registry/stories/webservice/xx-person.rst
@@ -368,9 +368,8 @@ To change its expiration date, use setExpirationDate(date).
     >>> print(salgado_landscape["date_expires"])
     None
 
-    >>> import pytz
-    >>> from datetime import datetime
-    >>> someday = datetime(2058, 8, 1, tzinfo=pytz.UTC)
+    >>> from datetime import datetime, timezone
+    >>> someday = datetime(2058, 8, 1, tzinfo=timezone.utc)
     >>> print(
     ...     webservice.named_post(
     ...         salgado_landscape["self_link"],
diff --git a/lib/lp/registry/subscribers.py b/lib/lp/registry/subscribers.py
index 5a30067..f0fbc97 100644
--- a/lib/lp/registry/subscribers.py
+++ b/lib/lp/registry/subscribers.py
@@ -7,9 +7,8 @@ __all__ = [
 ]
 
 import textwrap
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from lazr.restful.utils import get_current_browser_request
 from zope.security.proxy import removeSecurityProxy
 
@@ -136,7 +135,7 @@ class LicenseNotification:
     def _formatDate(now=None):
         """Return the date formatted for messages."""
         if now is None:
-            now = datetime.now(tz=pytz.UTC)
+            now = datetime.now(tz=timezone.utc)
         return now.strftime("%Y-%m-%d")
 
     def _addLicenseChangeToReviewWhiteboard(self):
diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
index 26bdb59..0d024c8 100644
--- a/lib/lp/registry/tests/test_distribution.py
+++ b/lib/lp/registry/tests/test_distribution.py
@@ -3,10 +3,9 @@
 
 """Tests for Distribution."""
 
-import datetime
 import json
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import soupmatchers
 from fixtures import FakeLogger
 from lazr.lifecycle.snapshot import Snapshot
@@ -895,9 +894,7 @@ class TestDistribution(TestCaseWithFactory):
         self.useContext(person_logged_in(owner))
 
         # The Distribution now has a complimentary commercial subscription.
-        new_expires_date = datetime.datetime.now(
-            pytz.UTC
-        ) - datetime.timedelta(1)
+        new_expires_date = datetime.now(timezone.utc) - timedelta(1)
         naked_subscription = removeSecurityProxy(
             distribution.commercial_subscription
         )
@@ -1920,8 +1917,8 @@ class TestDistributionWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
             )
             distro_url = api_url(distro)
 
-        now = datetime.datetime.now(tz=pytz.utc)
-        day = datetime.timedelta(days=1)
+        now = datetime.now(tz=timezone.utc)
+        day = timedelta(days=1)
 
         yesterday_response = self.webservice.named_get(
             distro_url,
@@ -1947,8 +1944,8 @@ class TestDistributionWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
             self.factory.makeQuestion(title="Crash with %s" % oopsid)
             distro = self.factory.makeDistribution()
             distro_url = api_url(distro)
-        now = datetime.datetime.now(tz=pytz.utc)
-        day = datetime.timedelta(days=1)
+        now = datetime.now(tz=timezone.utc)
+        day = timedelta(days=1)
 
         empty_response = self.webservice.named_get(
             distro_url,
@@ -2360,7 +2357,7 @@ class TestDistributionVulnerabilities(TestCaseWithFactory):
         distribution = self.factory.makeDistribution()
         owner = distribution.owner
         cve = self.factory.makeCVE(sequence="2022-1234")
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         with person_logged_in(owner):
             # The distribution owner can create a new vulnerability in
@@ -2461,7 +2458,7 @@ class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory):
         distribution = self.factory.makeDistribution()
         person = distribution.owner
         cve = self.factory.makeCVE("2022-1234")
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         vulnerability = removeSecurityProxy(
             self.factory.makeVulnerability(
                 distribution,
@@ -2542,7 +2539,7 @@ class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory):
         distribution = self.factory.makeDistribution(owner=person)
         cve = self.factory.makeCVE("2022-1234")
         another_cve = self.factory.makeCVE("2022-1235")
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         first_vulnerability = removeSecurityProxy(
             self.factory.makeVulnerability(
@@ -2773,7 +2770,7 @@ class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory):
         distribution_url = api_base + api_url(distribution)
         owner_url = api_base + api_url(owner)
         cve_url = api_base + api_url(cve)
-        now = datetime.datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         self.assertEqual(0, distribution.vulnerabilities.count())
 
diff --git a/lib/lp/registry/tests/test_distro_webservice.py b/lib/lp/registry/tests/test_distro_webservice.py
index be1cf90..9bf685f 100644
--- a/lib/lp/registry/tests/test_distro_webservice.py
+++ b/lib/lp/registry/tests/test_distro_webservice.py
@@ -2,9 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import json
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -56,7 +55,7 @@ class TestGetBranchTips(TestCaseWithFactory):
             sourcepackage=source_package
         )
         registrant = self.factory.makePerson()
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         sourcepackagename = self.factory.makeSourcePackageName()
         SeriesSourcePackageBranchSet.new(
             series_1,
diff --git a/lib/lp/registry/tests/test_distroseries_vocabularies.py b/lib/lp/registry/tests/test_distroseries_vocabularies.py
index c9e5a39..322adf2 100644
--- a/lib/lp/registry/tests/test_distroseries_vocabularies.py
+++ b/lib/lp/registry/tests/test_distroseries_vocabularies.py
@@ -3,9 +3,8 @@
 
 """Tests for distroseries vocabularies in `lp.registry.vocabularies`."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-from pytz import utc
 from testtools.matchers import Equals, Not
 from zope.component import getUtility
 from zope.schema.interfaces import ITokenizedTerm, IVocabularyFactory
@@ -171,7 +170,7 @@ class TestDistroSeriesDerivationVocabulary(TestCaseWithFactory):
     def test_ordering(self):
         # The vocabulary is sorted by distribution display name then by the
         # date the distroseries was created, newest first.
-        now = datetime.now(utc)
+        now = datetime.now(timezone.utc)
         two_days_ago = now - timedelta(2)
         six_days_ago = now - timedelta(7)
 
diff --git a/lib/lp/registry/tests/test_oopsreferences.py b/lib/lp/registry/tests/test_oopsreferences.py
index 67c5269..8730672 100644
--- a/lib/lp/registry/tests/test_oopsreferences.py
+++ b/lib/lp/registry/tests/test_oopsreferences.py
@@ -3,9 +3,7 @@
 
 """Tests of the oopsreferences core."""
 
-from datetime import datetime, timedelta
-
-from pytz import utc
+from datetime import datetime, timedelta, timezone
 
 from lp.registry.model.oopsreferences import referenced_oops
 from lp.services.database.interfaces import IStore
@@ -26,7 +24,7 @@ class TestOopsReferences(TestCaseWithFactory):
         oopsid = "OOPS-abcdef1234"
         MessageSet().fromText("foo", "foo %s bar" % oopsid)
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid}, referenced_oops(now - day, now, "product=1", {})
@@ -40,7 +38,7 @@ class TestOopsReferences(TestCaseWithFactory):
         self.factory.makeEmailMessage()
         MessageSet().fromText("Crash with %s" % oopsid, "body")
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid}, referenced_oops(now - day, now, "product=1", {})
@@ -55,7 +53,7 @@ class TestOopsReferences(TestCaseWithFactory):
         with person_logged_in(bug.owner):
             bug.title = "Crash with %s" % oopsid
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid}, referenced_oops(now - day, now, "product=1", {})
@@ -70,7 +68,7 @@ class TestOopsReferences(TestCaseWithFactory):
         with person_logged_in(bug.owner):
             bug.description = "Crash with %s" % oopsid
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid}, referenced_oops(now - day, now, "product=1", {})
@@ -83,7 +81,7 @@ class TestOopsReferences(TestCaseWithFactory):
         oopsid = "OOPS-abcdef1234"
         question = self.factory.makeQuestion(title="Crash with %s" % oopsid)
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid},
@@ -108,7 +106,7 @@ class TestOopsReferences(TestCaseWithFactory):
         oopsid = "OOPS-abcdef1234"
         question = self.factory.makeQuestion(title="Crash with %s" % oopsid)
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.store.flush()
         self.assertEqual(
@@ -127,7 +125,7 @@ class TestOopsReferences(TestCaseWithFactory):
             description="Crash with %s" % oopsid
         )
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid},
@@ -154,7 +152,7 @@ class TestOopsReferences(TestCaseWithFactory):
         with person_logged_in(question.owner):
             question.whiteboard = "Crash with %s" % oopsid
             self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid},
@@ -182,7 +180,7 @@ class TestOopsReferences(TestCaseWithFactory):
         with person_logged_in(question.owner):
             question.whiteboard = "Crash with %s" % oopsid
             self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid},
@@ -223,7 +221,7 @@ class TestOopsReferences(TestCaseWithFactory):
                 % oopsid_new
             )
         self.store.flush()
-        now = datetime.now(tz=utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             {oopsid_old, oopsid_new},
diff --git a/lib/lp/registry/tests/test_person.py b/lib/lp/registry/tests/test_person.py
index 2fa9c5c..c264f39 100644
--- a/lib/lp/registry/tests/test_person.py
+++ b/lib/lp/registry/tests/test_person.py
@@ -2,9 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restful.utils import smartquote
 from storm.locals import Desc
@@ -918,7 +917,7 @@ class TestPersonStates(TestCaseWithFactory):
         self.guadamen = person_set.getByName("guadamen")
         product_set = getUtility(IProductSet)
         self.bzr = product_set.getByName("bzr")
-        self.now = datetime.now(pytz.UTC)
+        self.now = datetime.now(timezone.utc)
 
     def test_canDeactivate_private_projects(self):
         """A user owning non-public products cannot be deactivated."""
@@ -2101,7 +2100,7 @@ class TestSpecifications(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.date_created = datetime.now(pytz.utc)
+        self.date_created = datetime.now(timezone.utc)
 
     def makeSpec(
         self,
diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py
index ee50de9..16ce58f 100644
--- a/lib/lp/registry/tests/test_personmerge.py
+++ b/lib/lp/registry/tests/test_personmerge.py
@@ -3,10 +3,9 @@
 
 """Tests for merge_people."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 from operator import attrgetter
 
-import pytz
 import transaction
 from testtools.matchers import (
     Equals,
@@ -177,7 +176,7 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
         # Verify that the oldest datecreated is merged.
         person = self.factory.makePerson()
         duplicate = self.factory.makePerson()
-        oldest_date = datetime(2005, 11, 25, 0, 0, 0, 0, pytz.timezone("UTC"))
+        oldest_date = datetime(2005, 11, 25, 0, 0, 0, 0, timezone.utc)
         removeSecurityProxy(duplicate).datecreated = oldest_date
         self._do_premerge(duplicate, person)
         login_person(person)
diff --git a/lib/lp/registry/tests/test_personnotification.py b/lib/lp/registry/tests/test_personnotification.py
index 3195949..027fa0d 100644
--- a/lib/lp/registry/tests/test_personnotification.py
+++ b/lib/lp/registry/tests/test_personnotification.py
@@ -4,9 +4,8 @@
 """Test the PersonNotification classes."""
 
 import logging
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -113,9 +112,7 @@ class TestPersonNotificationManager(TestCaseWithFactory):
         )
         age = timedelta(days=int(config.person_notification.retained_days) + 1)
         naked_notification = removeSecurityProxy(notification)
-        naked_notification.date_created = (
-            datetime.now(pytz.timezone("UTC")) - age
-        )
+        naked_notification.date_created = datetime.now(timezone.utc) - age
         self.manager.purgeNotifications()
         notifications = self.notification_set.getNotificationsToSend()
         self.assertEqual(0, notifications.count())
diff --git a/lib/lp/registry/tests/test_poll.py b/lib/lp/registry/tests/test_poll.py
index 2e96a4d..61dea70 100644
--- a/lib/lp/registry/tests/test_poll.py
+++ b/lib/lp/registry/tests/test_poll.py
@@ -1,10 +1,9 @@
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from operator import attrgetter
 
-import pytz
 from testtools.matchers import ContainsDict, Equals, MatchesListwise
 from zope.component import getUtility
 
@@ -34,10 +33,10 @@ class TestPoll(TestCaseWithFactory):
         team = self.factory.makeTeam(owner)
         poll = self.factory.makePoll(team, "name", "title", "proposition")
         # Force opening of poll so that we can vote.
-        poll.dateopens = datetime.now(pytz.UTC) - timedelta(minutes=2)
+        poll.dateopens = datetime.now(timezone.utc) - timedelta(minutes=2)
         poll.storeSimpleVote(owner, None)
         # Force closing of the poll so that we can call getWinners().
-        poll.datecloses = datetime.now(pytz.UTC)
+        poll.datecloses = datetime.now(timezone.utc)
         self.assertIsNone(poll.getWinners(), poll.getWinners())
 
 
@@ -73,7 +72,7 @@ class TestPollWebservice(TestCaseWithFactory):
         polls = []
         for team in teams:
             for offset in (-8, -1, 1):
-                dateopens = datetime.now(pytz.UTC) + timedelta(days=offset)
+                dateopens = datetime.now(timezone.utc) + timedelta(days=offset)
                 datecloses = dateopens + timedelta(days=7)
                 polls.append(
                     getUtility(IPollSet).new(
diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
index 6f28d7b..208ae3d 100644
--- a/lib/lp/registry/tests/test_product.py
+++ b/lib/lp/registry/tests/test_product.py
@@ -1,10 +1,9 @@
 # Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from io import BytesIO
 
-import pytz
 import transaction
 from storm.locals import Store
 from testtools.matchers import MatchesAll
@@ -799,7 +798,7 @@ class TestProduct(TestCaseWithFactory):
         self.useContext(person_logged_in(owner))
 
         # The Product now has a complimentary commercial subscription.
-        new_expires_date = datetime.now(pytz.timezone("UTC")) - timedelta(1)
+        new_expires_date = datetime.now(timezone.utc) - timedelta(1)
         naked_subscription = removeSecurityProxy(
             product.commercial_subscription
         )
@@ -1909,7 +1908,7 @@ class ProductLicensingTestCase(TestCaseWithFactory):
             cs = product.commercial_subscription
             self.assertIsNotNone(cs)
             self.assertIn("complimentary-30-day", cs.sales_system_id)
-            now = datetime.now(pytz.UTC)
+            now = datetime.now(timezone.utc)
             self.assertTrue(now >= cs.date_starts)
             future_30_days = now + timedelta(days=30)
             self.assertTrue(future_30_days >= cs.date_expires)
@@ -1939,7 +1938,7 @@ class ProductLicensingTestCase(TestCaseWithFactory):
             cs = product.commercial_subscription
             self.assertIsNotNone(cs)
             self.assertIn("complimentary-30-day", cs.sales_system_id)
-            now = datetime.now(pytz.UTC)
+            now = datetime.now(timezone.utc)
             self.assertTrue(now >= cs.date_starts)
             future_30_days = now + timedelta(days=30)
             self.assertTrue(future_30_days >= cs.date_expires)
@@ -2289,7 +2288,7 @@ class TestSpecifications(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.date_created = datetime.now(pytz.utc)
+        self.date_created = datetime.now(timezone.utc)
 
     def makeSpec(
         self,
@@ -2543,7 +2542,7 @@ class TestWebService(WebServiceTestCase):
         product = question.product
         transaction.commit()
         ws_product = self.wsObject(product, product.owner)
-        now = datetime.now(tz=pytz.utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             [oopsid],
@@ -2564,7 +2563,7 @@ class TestWebService(WebServiceTestCase):
         product = self.factory.makeProduct()
         transaction.commit()
         ws_product = self.wsObject(product, product.owner)
-        now = datetime.now(tz=pytz.utc)
+        now = datetime.now(tz=timezone.utc)
         day = timedelta(days=1)
         self.assertEqual(
             [],
diff --git a/lib/lp/registry/tests/test_productjob.py b/lib/lp/registry/tests/test_productjob.py
index c6e2961..5de315e 100644
--- a/lib/lp/registry/tests/test_productjob.py
+++ b/lib/lp/registry/tests/test_productjob.py
@@ -3,9 +3,8 @@
 
 """Tests for ProductJobs."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from testtools.content import text_content
 from zope.component import getUtility
@@ -80,7 +79,7 @@ class CommercialHelpers:
 
     def make_test_products(self):
         products = {}
-        now = datetime.now(pytz.utc)
+        now = datetime.now(timezone.utc)
         products["approved"] = self.factory.makeProduct(licenses=[License.MIT])
         products["expired"] = self.make_expiring_product(now - timedelta(1))
         products["expired_with_job"] = self.make_expiring_product(
@@ -290,7 +289,7 @@ class ProductJobDerivedTestCase(TestCaseWithFactory):
 
     def test_find_date_since(self):
         # Find all the jobs for a product since a date regardless of job_type.
-        now = datetime.now(pytz.utc)
+        now = datetime.now(timezone.utc)
         seven_days_ago = now - timedelta(7)
         thirty_days_ago = now - timedelta(30)
         product = self.factory.makeProduct()
diff --git a/lib/lp/registry/tests/test_project_milestone.py b/lib/lp/registry/tests/test_project_milestone.py
index d03f172..918a009 100644
--- a/lib/lp/registry/tests/test_project_milestone.py
+++ b/lib/lp/registry/tests/test_project_milestone.py
@@ -4,9 +4,8 @@
 """Project Milestone related test helper."""
 
 import unittest
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from lazr.restfulclient.errors import ClientError
 from storm.store import Store
 from zope.component import getUtility
@@ -338,7 +337,7 @@ class TestDuplicateProductReleases(TestCaseWithFactory):
         product = product_set["evolution"]
         series = product.getSeries("trunk")
         milestone = series.newMilestone(name="1.1", dateexpected=None)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         milestone.createProductRelease(1, now)
         self.assertRaises(
             MultipleProductReleases, milestone.createProductRelease, 1, now
@@ -358,7 +357,7 @@ class TestDuplicateProductReleases(TestCaseWithFactory):
 
         project = launchpad.projects["evolution"]
         milestone = project.getMilestone(name="2.1.6")
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
 
         e = self.assertRaises(
             ClientError, milestone.createProductRelease, date_released=now
diff --git a/lib/lp/registry/tests/test_subscribers.py b/lib/lp/registry/tests/test_subscribers.py
index b57ef0c..3af751e 100644
--- a/lib/lp/registry/tests/test_subscribers.py
+++ b/lib/lp/registry/tests/test_subscribers.py
@@ -3,9 +3,8 @@
 
 """Test subscruber classes and functions."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 from lazr.restful.utils import get_current_browser_request
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -264,7 +263,7 @@ class LicenseNotificationTestCase(TestCaseWithFactory):
 
     def test_formatDate(self):
         # Verify the date format.
-        now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
+        now = datetime(2005, 6, 15, 0, 0, 0, 0, timezone.utc)
         result = LicenseNotification._formatDate(now)
         self.assertEqual("2005-06-15", result)
 
diff --git a/lib/lp/registry/tests/test_teammembership.py b/lib/lp/registry/tests/test_teammembership.py
index 5d0796f..403d7cd 100644
--- a/lib/lp/registry/tests/test_teammembership.py
+++ b/lib/lp/registry/tests/test_teammembership.py
@@ -5,10 +5,9 @@ import bz2
 import pickle
 import re
 import subprocess
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from unittest import TestLoader
 
-import pytz
 import transaction
 from fixtures import TempDir
 from testtools.content import text_content
@@ -111,7 +110,7 @@ class TestTeamMembershipSetScripts(TestCaseWithFactory):
         teammembership = membershipset.getByPersonAndTeam(person, team)
 
         # Set expiration time to now
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         removeSecurityProxy(teammembership).dateexpires = now
 
         janitor = getUtility(ILaunchpadCelebrities).janitor
@@ -167,7 +166,7 @@ class TestTeamMembershipSet(TestCaseWithFactory):
         )
         self.assertEqual(ubuntu_team.teamowner, membership.proposed_by)
         self.assertEqual(membership.proponent_comment, "I like her")
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         self.assertTrue(membership.date_proposed <= now)
         self.assertTrue(membership.datejoined <= now)
         self.assertEqual(ubuntu_team.teamowner, membership.reviewed_by)
@@ -192,7 +191,7 @@ class TestTeamMembershipSet(TestCaseWithFactory):
         )
         self.assertEqual(marilize, membership.proposed_by)
         self.assertEqual(membership.proponent_comment, "I'd like to join")
-        self.assertTrue(membership.date_proposed <= datetime.now(pytz.UTC))
+        self.assertTrue(membership.date_proposed <= datetime.now(timezone.utc))
         self.assertEqual(membership.reviewed_by, None)
         self.assertEqual(membership.acknowledged_by, None)
 
@@ -225,7 +224,7 @@ class TestTeamMembershipSet(TestCaseWithFactory):
         # Now we need to cheat and set the expiration date of both memberships
         # manually because otherwise we would only be allowed to set an
         # expiration date in the future.
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         sample_person_on_motu = removeSecurityProxy(
             self.membershipset.getByPersonAndTeam(sample_person, motu)
         )
@@ -282,7 +281,7 @@ class TestTeamMembershipSet(TestCaseWithFactory):
         self.assertEqual([], list(member.teams_participated_in))
 
     def test_getMembershipsExpiringOnDates(self):
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         datetime1 = now + timedelta(days=1)
         datetime2 = now + timedelta(days=2)
         datetime3 = now + timedelta(days=3)
@@ -305,7 +304,7 @@ class TestTeamMembershipSet(TestCaseWithFactory):
 
     def test_getExpiringMembershipsToWarn(self):
         team = self.factory.makeTeam(name="super")
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         login_celebrity("admin")
 
         memberships = [
@@ -871,7 +870,7 @@ class TestTeamMembershipSetStatus(TestCaseWithFactory):
             )
             self.assertEqual(tm.proposed_by, self.foobar)
             self.assertEqual(tm.proponent_comment, "Did it 'cause I can")
-            self.assertTrue(tm.date_proposed <= datetime.now(pytz.UTC))
+            self.assertTrue(tm.date_proposed <= datetime.now(timezone.utc))
             # Destroy the membership so that we can create another in a
             # different state.
             tm.destroySelf()
@@ -901,7 +900,7 @@ class TestTeamMembershipSetStatus(TestCaseWithFactory):
             tm.setStatus(status, self.foobar, "Did it 'cause I can")
             self.assertEqual(tm.acknowledged_by, self.foobar)
             self.assertEqual(tm.acknowledger_comment, "Did it 'cause I can")
-            self.assertTrue(tm.date_acknowledged <= datetime.now(pytz.UTC))
+            self.assertTrue(tm.date_acknowledged <= datetime.now(timezone.utc))
             # Destroy the membership so that we can create another in a
             # different state.
             tm.destroySelf()
@@ -938,7 +937,7 @@ class TestTeamMembershipSetStatus(TestCaseWithFactory):
                 tm.setStatus(new_status, self.foobar, "Did it 'cause I can")
                 self.assertEqual(tm.reviewed_by, self.foobar)
                 self.assertEqual(tm.reviewer_comment, "Did it 'cause I can")
-                self.assertTrue(tm.date_reviewed <= datetime.now(pytz.UTC))
+                self.assertTrue(tm.date_reviewed <= datetime.now(timezone.utc))
 
                 # Destroy the membership so that we can create another in a
                 # different state.
@@ -957,7 +956,7 @@ class TestTeamMembershipSetStatus(TestCaseWithFactory):
             tm.datejoined, "There can be no datejoined at this point."
         )
         tm.setStatus(TeamMembershipStatus.APPROVED, self.foobar)
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         self.assertTrue(tm.datejoined <= now)
 
         # We now set the status to deactivated and change datejoined to a
@@ -1293,7 +1292,7 @@ class TestTeamMembershipSendExpirationWarningEmail(TestCaseWithFactory):
 
     def test_message_sent_for_future_expiration(self):
         # An email is sent to the user whose membership will expire.
-        tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
+        tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
         removeSecurityProxy(self.tm).dateexpires = tomorrow
         self.tm.sendExpirationWarningEmail()
         notifications = self.runMailJobs()
@@ -1306,7 +1305,7 @@ class TestTeamMembershipSendExpirationWarningEmail(TestCaseWithFactory):
 
     def test_no_message_sent_for_expired_memberships(self):
         # Members whose membership has expired do not get a message.
-        yesterday = datetime.now(pytz.UTC) - timedelta(days=1)
+        yesterday = datetime.now(timezone.utc) - timedelta(days=1)
         removeSecurityProxy(self.tm).dateexpires = yesterday
         self.tm.sendExpirationWarningEmail()
         notifications = self.runMailJobs()
@@ -1317,7 +1316,7 @@ class TestTeamMembershipSendExpirationWarningEmail(TestCaseWithFactory):
         with person_logged_in(self.member):
             self.member.deactivate("Goodbye.")
         IStore(self.member).flush()
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)
         self.tm.sendExpirationWarningEmail()
         notifications = self.runMailJobs()
diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
index e798587..92ab040 100644
--- a/lib/lp/scripts/garbo.py
+++ b/lib/lp/scripts/garbo.py
@@ -17,10 +17,9 @@ import multiprocessing
 import os
 import threading
 import time
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 import iso8601
-import pytz
 import six
 import transaction
 from contrib.glock import GlobalLock, LockAlreadyAcquired
@@ -720,9 +719,7 @@ class PopulateLatestPersonSourcePackageReleaseCache(TunableLoop):
                 lpsprc_record.upload_distroseries_id,
                 lpsprc_record.sourcepackagename_id,
             )
-            existing_records[key] = pytz.UTC.localize(
-                lpsprc_record.dateuploaded
-            )
+            existing_records[key] = lpsprc_record.dateuploaded
 
         # Gather account statuses for creators and maintainers.
         # Deactivating or closing an account removes its LPSPRC rows, and we
@@ -927,7 +924,7 @@ class RevisionCachePruner(TunableLoop):
 
     def isDone(self):
         """We are done when there are no old revisions to delete."""
-        epoch = datetime.now(pytz.UTC) - timedelta(days=30)
+        epoch = datetime.now(timezone.utc) - timedelta(days=30)
         store = IPrimaryStore(RevisionCache)
         results = store.find(
             RevisionCache, RevisionCache.revision_date < epoch
diff --git a/lib/lp/scripts/harness.py b/lib/lp/scripts/harness.py
index a9ccdd8..fe79099 100644
--- a/lib/lp/scripts/harness.py
+++ b/lib/lp/scripts/harness.py
@@ -20,7 +20,6 @@ import sys
 import webbrowser
 
 import transaction
-from pytz import utc
 from storm.expr import *  # noqa: F401,F403
 
 # Bring in useful bits of Storm.
@@ -45,7 +44,6 @@ from lp.testing.factory import LaunchpadObjectFactory
 
 # Silence unused name warnings
 (
-    utc,
     transaction,
     verifyObject,
     removeSecurityProxy,
diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
index 1f0a51c..796866f 100644
--- a/lib/lp/scripts/tests/test_garbo.py
+++ b/lib/lp/scripts/tests/test_garbo.py
@@ -10,13 +10,12 @@ import io
 import logging
 import re
 import time
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from functools import partial
 from textwrap import dedent
 
 import transaction
 from psycopg2 import IntegrityError
-from pytz import UTC
 from storm.exceptions import LostObjectError
 from storm.expr import SQL, In, Min, Not
 from storm.locals import Int
@@ -266,7 +265,7 @@ class TestSessionPruner(TestCase):
         nuke_all_sessions()
         self.addCleanup(nuke_all_sessions)
 
-        recent = datetime.now(UTC)
+        recent = datetime.now(timezone.utc)
         yesterday = recent - timedelta(days=1)
         ancient = recent - timedelta(days=61)
 
@@ -370,7 +369,7 @@ class TestSessionPruner(TestCase):
             "ancient_unauth",
         }
 
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
 
         # Make some duplicate logins from a few days ago.
         # Only the most recent 6 will be kept. Oldest is 'old dupe 9',
@@ -506,7 +505,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         )
 
     def test_CodeImportResultPruner(self):
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         store = IPrimaryStore(CodeImportResult)
 
         results_to_keep_count = config.codeimport.consecutive_failure_limit - 1
@@ -561,12 +560,12 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         self.assertTrue(
             store.find(Min(CodeImportResult.date_created))
             .one()
-            .replace(tzinfo=UTC)
+            .replace(tzinfo=timezone.utc)
             >= now - timedelta(days=30)
         )
 
     def test_CodeImportEventPruner(self):
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         store = IPrimaryStore(CodeImportResult)
 
         switch_dbuser("testadmin")
@@ -595,7 +594,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         self.assertTrue(
             store.find(Min(CodeImportEvent.date_created))
             .one()
-            .replace(tzinfo=UTC)
+            .replace(tzinfo=timezone.utc)
             >= now - timedelta(days=30)
         )
 
@@ -655,7 +654,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
     def test_PreviewDiffPruner(self):
         switch_dbuser("testadmin")
         mp1 = self.factory.makeBranchMergeProposal()
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         self.factory.makePreviewDiff(
             merge_proposal=mp1, date_created=now - timedelta(hours=2)
         )
@@ -676,7 +675,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         # are not removed.
         switch_dbuser("testadmin")
         mp1 = self.factory.makeBranchMergeProposal()
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         mp1_diff_comment = self.factory.makePreviewDiff(
             merge_proposal=mp1, date_created=now - timedelta(hours=2)
         )
@@ -753,7 +752,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         self.factory.makePerson(name="test-unlinked-person-new")
         person_old = self.factory.makePerson(name="test-unlinked-person-old")
         removeSecurityProxy(person_old).datecreated = datetime(
-            2008, 1, 1, tzinfo=UTC
+            2008, 1, 1, tzinfo=timezone.utc
         )
 
         # Normally, the garbage collector will do nothing because the
@@ -1127,7 +1126,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
         switch_dbuser("testadmin")
         store = IPrimaryStore(GitRepository)
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         recently = now - timedelta(minutes=2)
         long_ago = now - timedelta(minutes=65)
 
@@ -1238,7 +1237,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             TimeLimitedToken(
                 path="sample path",
                 token=b"foo",
-                created=datetime(2008, 1, 1, tzinfo=UTC),
+                created=datetime(2008, 1, 1, tzinfo=timezone.utc),
             )
         )
         store.add(TimeLimitedToken(path="sample path", token=b"bar")),
@@ -1407,7 +1406,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         # each worker.
         switch_dbuser("testadmin")
         bug = self.factory.makeBug()
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         cutoff = now - timedelta(days=1)
         old_update = now - timedelta(days=2)
         naked_bug = removeSecurityProxy(bug)
@@ -1640,7 +1639,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[0],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 1, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 1, tzinfo=timezone.utc),
         )
         self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr1
@@ -1650,7 +1649,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[1],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 2, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 2, tzinfo=timezone.utc),
         )
         self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr2
@@ -1660,7 +1659,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[0],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 3, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 3, tzinfo=timezone.utc),
         )
         self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr3
@@ -1670,7 +1669,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[1],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 4, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 4, tzinfo=timezone.utc),
         )
         self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr4
@@ -1680,7 +1679,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[2],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 5, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 5, tzinfo=timezone.utc),
         )
         spph_1 = self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr5
@@ -1718,9 +1717,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
                         MatchesStructure(
                             creator=Equals(spr.creator),
                             maintainer_id=Is(None),
-                            dateuploaded=AfterPreprocessing(
-                                UTC.localize, Equals(spr.dateuploaded)
-                            ),
+                            dateuploaded=Equals(spr.dateuploaded),
                         )
                         for spr in sprs
                     ]
@@ -1740,9 +1737,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
                         MatchesStructure(
                             maintainer=Equals(spr.maintainer),
                             creator_id=Is(None),
-                            dateuploaded=AfterPreprocessing(
-                                UTC.localize, Equals(spr.dateuploaded)
-                            ),
+                            dateuploaded=Equals(spr.dateuploaded),
                         )
                         for spr in sprs
                     ]
@@ -1769,7 +1764,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             maintainer=maintainers[1],
             distroseries=distroseries,
             sourcepackagename=spn,
-            date_uploaded=datetime(2010, 12, 5, tzinfo=UTC),
+            date_uploaded=datetime(2010, 12, 5, tzinfo=timezone.utc),
         )
         spph_2 = self.factory.makeSourcePackagePublishingHistory(
             status=PackagePublishingStatus.PUBLISHED, sourcepackagerelease=spr6
@@ -1804,7 +1799,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         # `interval` days ago.  If `keep_binary_files_days` is given, set
         # that on the test LiveFS.  If `base_image` is True, install the
         # test LiveFS file as a base image for its DAS.
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         switch_dbuser("testadmin")
         self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: "on"}))
         store = IPrimaryStore(LiveFSFile)
@@ -1919,7 +1914,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         # remove snap files named each of `filenames` with a store upload
         # job of status `job_status` that finished more than `interval` days
         # ago.
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         switch_dbuser("testadmin")
         store = IPrimaryStore(SnapFile)
 
@@ -1992,7 +1987,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             ppa.newSubscription(self.factory.makePerson(), ppa.owner)
             for _ in range(2)
         ]
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         subs[0].date_expires = now - timedelta(minutes=3)
         self.assertEqual(ArchiveSubscriberStatus.CURRENT, subs[0].status)
         subs[1].date_expires = now + timedelta(minutes=3)
@@ -2242,7 +2237,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
         # Artifacts for report2 are newer than 90 days and
         # we expect them to survive the garbo job.
         switch_dbuser("testadmin")
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         report1 = removeSecurityProxy(self.factory.makeRevisionStatusReport())
         report2 = self.factory.makeRevisionStatusReport()
         report1.date_created = now - timedelta(days=120)
@@ -2494,7 +2489,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
 
     def test_ArchiveFileDatePopulator(self):
         switch_dbuser("testadmin")
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         archive_files = [self.factory.makeArchiveFile() for _ in range(2)]
         removeSecurityProxy(
             archive_files[1]
@@ -2530,7 +2525,9 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
 
         removeSecurityProxy(
             archive_files[1]
-        ).scheduled_deletion_date = datetime.now(UTC) + timedelta(days=1)
+        ).scheduled_deletion_date = datetime.now(timezone.utc) + timedelta(
+            days=1
+        )
         self.assertIsNone(archive_files[1].date_superseded)
 
         self.assertIsNone(archive_files[2].date_superseded)
@@ -2550,7 +2547,7 @@ class TestGarboTasks(TestCaseWithFactory):
 
     def test_LoginTokenPruner(self):
         store = IPrimaryStore(LoginToken)
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         switch_dbuser("testadmin")
 
         # It is configured as a daily task.
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 7b0a560..97053a9 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -17,9 +17,8 @@ __all__ = [
     "OnlyVcsImportsAndAdmins",
 ]
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.interface import Interface
 
 from lp.app.security import AuthorizationBase
@@ -76,7 +75,7 @@ class AnyLegitimatePerson(AuthorizationBase):
 
     def _isOldEnough(self, user):
         return datetime.now(
-            pytz.UTC
+            timezone.utc
         ) - user.person.account.date_created >= timedelta(
             days=config.launchpad.min_legitimate_account_age
         )
diff --git a/lib/lp/services/apachelogparser/base.py b/lib/lp/services/apachelogparser/base.py
index 32ea7dc..d6b3e00 100644
--- a/lib/lp/services/apachelogparser/base.py
+++ b/lib/lp/services/apachelogparser/base.py
@@ -4,9 +4,8 @@
 import gzip
 import os
 import struct
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import six
 from contrib import apachelog
 from lazr.uri import URI, InvalidURIError
@@ -181,7 +180,7 @@ def create_or_update_parsedlog_entry(first_line, parsed_bytes):
         ParsedApacheLog(first_line, parsed_bytes)
     else:
         parsed_file.bytes_read = parsed_bytes
-        parsed_file.date_last_parsed = datetime.now(pytz.UTC)
+        parsed_file.date_last_parsed = datetime.now(timezone.utc)
 
 
 def get_day(date):
diff --git a/lib/lp/services/auth/model.py b/lib/lp/services/auth/model.py
index f2e1d3a..abedcbf 100644
--- a/lib/lp/services/auth/model.py
+++ b/lib/lp/services/auth/model.py
@@ -9,9 +9,8 @@ __all__ = [
 ]
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.databases.postgres import JSON
 from storm.expr import SQL, And, Cast, Or, Select, Update
 from storm.locals import DateTime, Int, Reference, Unicode
@@ -38,7 +37,7 @@ class AccessToken(StormBase):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
 
     _token_sha256 = Unicode(name="token_sha256", allow_none=False)
@@ -54,10 +53,10 @@ class AccessToken(StormBase):
     _scopes = JSON(name="scopes", allow_none=False)
 
     date_last_used = DateTime(
-        name="date_last_used", tzinfo=pytz.UTC, allow_none=True
+        name="date_last_used", tzinfo=timezone.utc, allow_none=True
     )
     date_expires = DateTime(
-        name="date_expires", tzinfo=pytz.UTC, allow_none=True
+        name="date_expires", tzinfo=timezone.utc, allow_none=True
     )
 
     revoked_by_id = Int(name="revoked_by", allow_none=True)
@@ -129,7 +128,7 @@ class AccessToken(StormBase):
 
     @property
     def is_expired(self):
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         return self.date_expires is not None and self.date_expires <= now
 
     def revoke(self, revoked_by):
diff --git a/lib/lp/services/auth/tests/test_model.py b/lib/lp/services/auth/tests/test_model.py
index 81f3023..bfd8c20 100644
--- a/lib/lp/services/auth/tests/test_model.py
+++ b/lib/lp/services/auth/tests/test_model.py
@@ -6,9 +6,8 @@
 import hashlib
 import os
 import signal
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from storm.store import Store
 from testtools.matchers import Is, MatchesStructure
@@ -76,7 +75,7 @@ class TestAccessToken(TestCaseWithFactory):
         owner = self.factory.makePerson()
         _, token = self.factory.makeAccessToken(owner=owner)
         login_person(owner)
-        recent = datetime.now(pytz.UTC) - timedelta(minutes=1)
+        recent = datetime.now(timezone.utc) - timedelta(minutes=1)
         removeSecurityProxy(token).date_last_used = recent
         transaction.commit()
         token.updateLastUsed()
@@ -88,7 +87,7 @@ class TestAccessToken(TestCaseWithFactory):
         owner = self.factory.makePerson()
         _, token = self.factory.makeAccessToken(owner=owner)
         login_person(owner)
-        recent = datetime.now(pytz.UTC) - timedelta(hours=1)
+        recent = datetime.now(timezone.utc) - timedelta(hours=1)
         removeSecurityProxy(token).date_last_used = recent
         transaction.commit()
         token.updateLastUsed()
@@ -145,7 +144,7 @@ class TestAccessToken(TestCaseWithFactory):
         _, current_token = self.factory.makeAccessToken(owner=owner)
         _, expired_token = self.factory.makeAccessToken(
             owner=owner,
-            date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
+            date_expires=datetime.now(timezone.utc) - timedelta(minutes=1),
         )
         self.assertFalse(current_token.is_expired)
         self.assertTrue(expired_token.is_expired)
@@ -287,11 +286,11 @@ class TestAccessTokenSet(TestCaseWithFactory):
         _, current_token = self.factory.makeAccessToken(target=target)
         _, expires_soon_token = self.factory.makeAccessToken(
             target=target,
-            date_expires=datetime.now(pytz.UTC) + timedelta(hours=1),
+            date_expires=datetime.now(timezone.utc) + timedelta(hours=1),
         )
         _, expired_token = self.factory.makeAccessToken(
             target=target,
-            date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
+            date_expires=datetime.now(timezone.utc) - timedelta(minutes=1),
         )
         self.assertContentEqual(
             [current_token, expires_soon_token],
@@ -371,11 +370,11 @@ class TestAccessTokenSet(TestCaseWithFactory):
         _, current_token = self.factory.makeAccessToken(target=target)
         _, expires_soon_token = self.factory.makeAccessToken(
             target=target,
-            date_expires=datetime.now(pytz.UTC) + timedelta(hours=1),
+            date_expires=datetime.now(timezone.utc) + timedelta(hours=1),
         )
         _, expired_token = self.factory.makeAccessToken(
             target=target,
-            date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
+            date_expires=datetime.now(timezone.utc) - timedelta(minutes=1),
         )
         self.assertEqual(
             current_token,
@@ -435,7 +434,7 @@ class TestAccessTokenTargetBase:
                 owner=self.owner,
                 description="Expired",
                 target=self.target,
-                date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
+                date_expires=datetime.now(timezone.utc) - timedelta(minutes=1),
             )
         response = self.webservice.named_get(
             self.target_url, "getAccessTokens", api_version="devel"
diff --git a/lib/lp/services/database/datetimecol.py b/lib/lp/services/database/datetimecol.py
index 69a5725..b23a381 100644
--- a/lib/lp/services/database/datetimecol.py
+++ b/lib/lp/services/database/datetimecol.py
@@ -5,9 +5,10 @@
 
 __all__ = ["UtcDateTimeCol"]
 
-import pytz
+from datetime import timezone
+
 import storm.sqlobject
 
 
 class UtcDateTimeCol(storm.sqlobject.UtcDateTimeCol):
-    _kwargs = {"tzinfo": pytz.timezone("UTC")}
+    _kwargs = {"tzinfo": timezone.utc}
diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
index ba70a7d..c02d042 100644
--- a/lib/lp/services/database/sqlbase.py
+++ b/lib/lp/services/database/sqlbase.py
@@ -27,10 +27,9 @@ __all__ = [
 ]
 
 
-from datetime import datetime
+from datetime import datetime, timezone
 
 import psycopg2
-import pytz
 import storm
 import transaction
 from psycopg2.extensions import (
@@ -289,7 +288,7 @@ def get_transaction_timestamp(store):
     timestamp = store.execute(
         "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
     ).get_one()[0]
-    return timestamp.replace(tzinfo=pytz.timezone("UTC"))
+    return timestamp.replace(tzinfo=timezone.utc)
 
 
 def quote(x):
diff --git a/lib/lp/services/database/tests/test_bulk.py b/lib/lp/services/database/tests/test_bulk.py
index ba5972a..09c9e03 100644
--- a/lib/lp/services/database/tests/test_bulk.py
+++ b/lib/lp/services/database/tests/test_bulk.py
@@ -3,10 +3,9 @@
 
 """Test the bulk database functions."""
 
-import datetime
+from datetime import datetime, timezone
 
 import transaction
-from pytz import UTC
 from storm.exceptions import ClassInfoError
 from storm.expr import SQL
 from storm.info import get_obj_info
@@ -291,7 +290,7 @@ class TestCreate(TestCaseWithFactory):
                 bug,
                 person,
                 person,
-                datetime.datetime.now(UTC),
+                datetime.now(timezone.utc),
                 BugNotificationLevel.LIFECYCLE,
             )
             for person in people
diff --git a/lib/lp/services/features/model.py b/lib/lp/services/features/model.py
index f869499..29631b0 100644
--- a/lib/lp/services/features/model.py
+++ b/lib/lp/services/features/model.py
@@ -7,9 +7,8 @@ __all__ = [
     "getFeatureStore",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import six
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
@@ -64,7 +63,7 @@ class FeatureFlagChangelogEntry(StormBase):
     def __init__(self, diff, comment, person):
         super().__init__()
         self.diff = six.ensure_text(diff)
-        self.date_changed = datetime.now(pytz.timezone("UTC"))
+        self.date_changed = datetime.now(timezone.utc)
         self.comment = six.ensure_text(comment)
         self.person = person
 
diff --git a/lib/lp/services/features/tests/test_changelog.py b/lib/lp/services/features/tests/test_changelog.py
index ef57e50..3cb34b0 100644
--- a/lib/lp/services/features/tests/test_changelog.py
+++ b/lib/lp/services/features/tests/test_changelog.py
@@ -3,9 +3,7 @@
 
 """Tests for feature flag change log."""
 
-from datetime import datetime
-
-import pytz
+from datetime import datetime, timezone
 
 from lp.services.features.changelog import ChangeLog
 from lp.services.features.model import FeatureFlagChangelogEntry
@@ -26,11 +24,11 @@ class TestFeatureFlagChangelogEntry(TestCaseWithFactory):
     def test_FeatureFlagChangelogEntry_creation(self):
         # A FeatureFlagChangelogEntry has a diff and a date of change.
         person = self.factory.makePerson()
-        before = datetime.now(pytz.timezone("UTC"))
+        before = datetime.now(timezone.utc)
         feature_flag_change = FeatureFlagChangelogEntry(
             diff, "comment", person
         )
-        after = datetime.now(pytz.timezone("UTC"))
+        after = datetime.now(timezone.utc)
         self.assertEqual(diff, feature_flag_change.diff)
         self.assertEqual("comment", feature_flag_change.comment)
         self.assertEqual(person, feature_flag_change.person)
diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
index b00378a..a7a1a20 100644
--- a/lib/lp/services/gpg/handler.py
+++ b/lib/lp/services/gpg/handler.py
@@ -16,12 +16,11 @@ import subprocess
 import sys
 import tempfile
 from contextlib import contextmanager
-from datetime import datetime
+from datetime import datetime, timezone
 from io import BytesIO
 from urllib.parse import urlencode
 
 import gpgme
-import pytz
 import requests
 from lazr.restful.utils import get_current_browser_request
 from zope.component import getUtility
@@ -354,7 +353,7 @@ class GPGHandler:
         """Inject a key pair into the signing service."""
         secret_key = key.export()
         public_key = self.retrieveKey(key.fingerprint).export()
-        now = datetime.now().replace(tzinfo=pytz.UTC)
+        now = datetime.now().replace(tzinfo=timezone.utc)
         getUtility(ISigningKeySet).inject(
             SigningKeyType.OPENPGP,
             secret_key,
diff --git a/lib/lp/services/gpg/tests/test_gpghandler.py b/lib/lp/services/gpg/tests/test_gpghandler.py
index 653740d..ec2da7e 100644
--- a/lib/lp/services/gpg/tests/test_gpghandler.py
+++ b/lib/lp/services/gpg/tests/test_gpghandler.py
@@ -4,11 +4,10 @@
 import os
 import shutil
 import subprocess
-from datetime import datetime
+from datetime import datetime, timezone
 from unittest import mock
 
 import gpgme
-import pytz
 import responses
 import six
 from fixtures import Fixture, MockPatch
@@ -493,7 +492,7 @@ class TestGPGHandler(TestCase):
                         "Launchpad PPA for Celso "
                         "\xe1\xe9\xed\xf3\xfa Providelo"
                     ),
-                    Equals(now.replace(tzinfo=pytz.UTC)),
+                    Equals(now.replace(tzinfo=timezone.utc)),
                 ]
             ),
         )
diff --git a/lib/lp/services/job/model/job.py b/lib/lp/services/job/model/job.py
index b0455fa..b5ef078 100644
--- a/lib/lp/services/job/model/job.py
+++ b/lib/lp/services/job/model/job.py
@@ -11,11 +11,10 @@ __all__ = [
 ]
 
 
-import datetime
 import time
 from calendar import timegm
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from lazr.jobrunner.jobrunner import LeaseHeld
 from storm.expr import And, Or, Select
@@ -31,8 +30,6 @@ from lp.services.database.sqlbase import SQLBase
 from lp.services.database.sqlobject import StringCol
 from lp.services.job.interfaces.job import IJob, JobStatus, JobType
 
-UTC = pytz.timezone("UTC")
-
 
 class InvalidTransition(Exception):
     """Invalid transition from one job status to another attempted."""
@@ -121,7 +118,7 @@ class Job(SQLBase):
             return False
         if self.scheduled_start is None:
             return True
-        return self.scheduled_start <= datetime.datetime.now(UTC)
+        return self.scheduled_start <= datetime.now(timezone.utc)
 
     @classmethod
     def createMultiple(self, store, num_jobs, requester=None):
@@ -142,10 +139,10 @@ class Job(SQLBase):
         """See `IJob`."""
         if (
             self.lease_expires is not None
-            and self.lease_expires >= datetime.datetime.now(UTC)
+            and self.lease_expires >= datetime.now(timezone.utc)
         ):
             raise LeaseHeld
-        expiry = datetime.datetime.fromtimestamp(time.time() + duration, UTC)
+        expiry = datetime.fromtimestamp(time.time() + duration, timezone.utc)
         self.lease_expires = expiry
 
     def getTimeout(self):
@@ -160,7 +157,7 @@ class Job(SQLBase):
     def start(self, manage_transaction=False):
         """See `IJob`."""
         self._set_status(JobStatus.RUNNING)
-        self.date_started = datetime.datetime.now(UTC)
+        self.date_started = datetime.now(timezone.utc)
         self.date_finished = None
         self.attempt_count += 1
         if manage_transaction:
@@ -172,7 +169,7 @@ class Job(SQLBase):
         if manage_transaction:
             transaction.commit()
         self._set_status(JobStatus.COMPLETED)
-        self.date_finished = datetime.datetime.now(UTC)
+        self.date_finished = datetime.now(timezone.utc)
         if manage_transaction:
             transaction.commit()
 
@@ -181,7 +178,7 @@ class Job(SQLBase):
         if manage_transaction:
             transaction.abort()
         self._set_status(JobStatus.FAILED)
-        self.date_finished = datetime.datetime.now(UTC)
+        self.date_finished = datetime.now(timezone.utc)
         if manage_transaction:
             transaction.commit()
 
@@ -199,7 +196,7 @@ class Job(SQLBase):
             transaction.commit()
         if self.status != JobStatus.WAITING:
             self._set_status(JobStatus.WAITING)
-        self.date_finished = datetime.datetime.now(UTC)
+        self.date_finished = datetime.now(timezone.utc)
         # Release the lease to allow short retry delays to be effective.
         self.lease_expires = None
         if add_commit_hook is not None:
diff --git a/lib/lp/services/job/runner.py b/lib/lp/services/job/runner.py
index a69ad20..7210027 100644
--- a/lib/lp/services/job/runner.py
+++ b/lib/lp/services/job/runner.py
@@ -21,7 +21,7 @@ import logging
 import os
 import sys
 from calendar import timegm
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from resource import RLIMIT_AS, getrlimit, setrlimit
 from signal import SIGHUP, signal
 from uuid import uuid4
@@ -31,7 +31,6 @@ from ampoule import child, main, pool
 from lazr.delegates import delegate_to
 from lazr.jobrunner.jobrunner import JobRunner as LazrJobRunner
 from lazr.jobrunner.jobrunner import LeaseHeld
-from pytz import utc
 from storm.exceptions import LostObjectError
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, succeed
@@ -303,7 +302,9 @@ class BaseRunnableJob(BaseRunnableJobSource):
     def queue(self, manage_transaction=False, abort_transaction=False):
         """See `IJob`."""
         if self.job.attempt_count > 0:
-            self.job.scheduled_start = datetime.now(utc) + self.retry_delay
+            self.job.scheduled_start = (
+                datetime.now(timezone.utc) + self.retry_delay
+            )
         # If we're aborting the transaction, we probably don't want to
         # start the task again
         if manage_transaction and abort_transaction:
diff --git a/lib/lp/services/job/tests/test_celery.py b/lib/lp/services/job/tests/test_celery.py
index df50435..effbc90 100644
--- a/lib/lp/services/job/tests/test_celery.py
+++ b/lib/lp/services/job/tests/test_celery.py
@@ -4,7 +4,7 @@
 """Tests for running jobs via Celery."""
 
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from time import sleep
 from unittest import mock
 
@@ -12,7 +12,6 @@ import iso8601
 import transaction
 from lazr.delegates import delegate_to
 from lazr.jobrunner.celerytask import drain_queues
-from pytz import UTC
 from testtools.matchers import (
     GreaterThan,
     HasLength,
@@ -85,7 +84,7 @@ class TestJobWithRetryError(TestJob):
         if self.job.attempt_count == 1:
             # First test without a conflicting lease. The job should be
             # rescheduled for 5 seconds (retry_delay) in the future.
-            self.job.lease_expires = datetime.now(UTC)
+            self.job.lease_expires = datetime.now(timezone.utc)
             raise RetryException
         elif self.job.attempt_count == 2:
             # The retry delay is 5 seconds, but the lease is for nearly 10
@@ -122,7 +121,7 @@ class TestJobsViaCelery(TestCaseWithFactory):
         self.useFixture(
             FeatureFixture({"jobs.celery.enabled_classes": "TestJob"})
         )
-        now = datetime.now(UTC)
+        now = datetime.now(timezone.utc)
         job_past = TestJob(scheduled_start=now - timedelta(seconds=60))
         job_past.celeryRunOnCommit()
         self.assertTrue(job_past.is_runnable)
@@ -167,7 +166,7 @@ class TestJobsViaCelery(TestCaseWithFactory):
         # Set scheduled_start on the job to ensure that retry delays
         # override it.
         job = TestJobWithRetryError(
-            scheduled_start=datetime.now(UTC) + timedelta(seconds=1)
+            scheduled_start=datetime.now(timezone.utc) + timedelta(seconds=1)
         )
         job.celeryRunOnCommit()
         transaction.commit()
diff --git a/lib/lp/services/job/tests/test_job.py b/lib/lp/services/job/tests/test_job.py
index f7d8c0b..900daaf 100644
--- a/lib/lp/services/job/tests/test_job.py
+++ b/lib/lp/services/job/tests/test_job.py
@@ -2,12 +2,10 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import time
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 import transaction
 from lazr.jobrunner.jobrunner import LeaseHeld
-from pytz import UTC
 from storm.locals import Store
 from testtools.matchers import Equals
 
@@ -250,7 +248,7 @@ class TestJob(TestCaseWithFactory):
         """is_runnable is false when the job is scheduled in the future."""
         job = Job(
             _status=JobStatus.WAITING,
-            scheduled_start=datetime.now(UTC) + timedelta(seconds=60),
+            scheduled_start=datetime.now(timezone.utc) + timedelta(seconds=60),
         )
         self.assertFalse(job.is_runnable)
 
@@ -258,7 +256,7 @@ class TestJob(TestCaseWithFactory):
         """is_runnable is true when the job is scheduled in the past."""
         job = Job(
             _status=JobStatus.WAITING,
-            scheduled_start=datetime.now(UTC) - timedelta(seconds=60),
+            scheduled_start=datetime.now(timezone.utc) - timedelta(seconds=60),
         )
         self.assertTrue(job.is_runnable)
 
@@ -405,7 +403,7 @@ class TestReadiness(TestCase):
     def test_ready_jobs_lease_expired(self):
         """Job.ready_jobs should include jobs with expired leases."""
         preexisting = self._sampleData()
-        UNIX_EPOCH = datetime.fromtimestamp(0, pytz.timezone("UTC"))
+        UNIX_EPOCH = datetime.fromtimestamp(0, timezone.utc)
         job = Job(lease_expires=UNIX_EPOCH)
         self.assertEqual(
             preexisting + [(job.id,)],
@@ -415,9 +413,7 @@ class TestReadiness(TestCase):
     def test_ready_jobs_lease_in_future(self):
         """Job.ready_jobs should not include jobs with active leases."""
         preexisting = self._sampleData()
-        future = datetime.fromtimestamp(
-            time.time() + 1000, pytz.timezone("UTC")
-        )
+        future = datetime.fromtimestamp(time.time() + 1000, timezone.utc)
         job = Job(lease_expires=future)
         self.assertEqual(
             preexisting, list(Store.of(job).execute(Job.ready_jobs))
@@ -428,9 +424,7 @@ class TestReadiness(TestCase):
         future.
         """
         preexisting = self._sampleData()
-        future = datetime.fromtimestamp(
-            time.time() + 1000, pytz.timezone("UTC")
-        )
+        future = datetime.fromtimestamp(time.time() + 1000, timezone.utc)
         job = Job(scheduled_start=future)
         self.assertEqual(
             preexisting, list(Store.of(job).execute(Job.ready_jobs))
diff --git a/lib/lp/services/job/tests/test_runner.py b/lib/lp/services/job/tests/test_runner.py
index e5fdaba..071f2ee 100644
--- a/lib/lp/services/job/tests/test_runner.py
+++ b/lib/lp/services/job/tests/test_runner.py
@@ -6,14 +6,13 @@
 import logging
 import re
 import sys
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from textwrap import dedent
 from time import sleep
 
 import transaction
 from lazr.jobrunner.jobrunner import LeaseHeld, SuspendJobException
 from lazr.restful.utils import get_current_browser_request
-from pytz import UTC
 from storm.locals import Bool, Int, Reference
 from testtools.matchers import GreaterThan, LessThan, MatchesAll, MatchesRegex
 from testtools.testcase import ExpectedException
@@ -408,7 +407,7 @@ class TestJobRunner(StatsMixin, TestCaseWithFactory):
         self.addCleanup(lambda: self.addDetail("log", logger.content))
         runner.runJob(job, None)
         self.assertEqual(JobStatus.WAITING, job.status)
-        expected_delay = datetime.now(UTC) + timedelta(minutes=10)
+        expected_delay = datetime.now(timezone.utc) + timedelta(minutes=10)
         self.assertThat(
             job.scheduled_start,
             MatchesAll(
diff --git a/lib/lp/services/librarian/doc/librarian.rst b/lib/lp/services/librarian/doc/librarian.rst
index 39099b6..d24ea39 100644
--- a/lib/lp/services/librarian/doc/librarian.rst
+++ b/lib/lp/services/librarian/doc/librarian.rst
@@ -274,14 +274,13 @@ If we abort the transaction, it is still in there
 
 You can also set the expiry date on the file this way too:
 
-    >>> from datetime import date, datetime
-    >>> from pytz import utc
+    >>> from datetime import date, datetime, timezone
     >>> url = client.remoteAddFile(
     ...     "text.txt",
     ...     len(data),
     ...     io.BytesIO(data),
     ...     "text/plain",
-    ...     expires=datetime(2005, 9, 1, 12, 0, 0, tzinfo=utc),
+    ...     expires=datetime(2005, 9, 1, 12, 0, 0, tzinfo=timezone.utc),
     ... )
     >>> transaction.abort()
 
@@ -774,7 +773,7 @@ The .last_downloaded property gives us the time delta from today to the
 day that file was last downloaded, or None if it's never been
 downloaded.
 
-    >>> today = datetime.now(utc).date()
+    >>> today = datetime.now(timezone.utc).date()
     >>> public_file.last_downloaded == today - last_downloaded_date
     True
 
diff --git a/lib/lp/services/librarian/interfaces/__init__.py b/lib/lp/services/librarian/interfaces/__init__.py
index f8e2401..bd5010e 100644
--- a/lib/lp/services/librarian/interfaces/__init__.py
+++ b/lib/lp/services/librarian/interfaces/__init__.py
@@ -12,10 +12,9 @@ __all__ = [
     "NEVER_EXPIRES",
 ]
 
-from datetime import datetime
+from datetime import datetime, timezone
 
 from lazr.restful.fields import Reference
-from pytz import utc
 from zope.interface import Attribute, Interface
 from zope.schema import Bool, Choice, Date, Datetime, Int, TextLine
 
@@ -26,7 +25,7 @@ from lp.services.librarian.interfaces.client import (
 
 # Set the expires attribute to this constant to flag a file that
 # should never be removed from the Librarian.
-NEVER_EXPIRES = datetime(2038, 1, 1, 0, 0, 0, tzinfo=utc)
+NEVER_EXPIRES = datetime(2038, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
 
 
 class ILibraryFileAlias(Interface):
diff --git a/lib/lp/services/librarian/model.py b/lib/lp/services/librarian/model.py
index 7aaf5de..83ca952 100644
--- a/lib/lp/services/librarian/model.py
+++ b/lib/lp/services/librarian/model.py
@@ -11,10 +11,9 @@ __all__ = [
 ]
 
 import hashlib
-from datetime import datetime
+from datetime import datetime, timezone
 from urllib.parse import urlparse
 
-import pytz
 from lazr.delegates import delegate_to
 from storm.locals import Date, Desc, Int, Reference, ReferenceSet, Store
 from zope.component import adapter, getUtility
@@ -180,7 +179,7 @@ class LibraryFileAlias(SQLBase):
         if entry is None:
             return None
         else:
-            return datetime.now(pytz.utc).date() - entry.day
+            return datetime.now(timezone.utc).date() - entry.day
 
     def updateDownloadCount(self, day, country, count):
         """See ILibraryFileAlias."""
@@ -317,7 +316,7 @@ class LibraryFileAliasSet:
         results.config(
             distinct=(LibraryFileDownloadCount.libraryfilealias_id,)
         )
-        now = datetime.now(pytz.utc).date()
+        now = datetime.now(timezone.utc).date()
         lfas_by_id = {lfa.id: lfa for lfa in lfas}
         for lfa_id, day in results:
             get_property_cache(lfas_by_id[lfa_id]).last_downloaded = now - day
diff --git a/lib/lp/services/librarian/smoketest.py b/lib/lp/services/librarian/smoketest.py
index 4291768..ba8299b 100644
--- a/lib/lp/services/librarian/smoketest.py
+++ b/lib/lp/services/librarian/smoketest.py
@@ -6,12 +6,11 @@
 """Perform simple librarian operations to verify the current configuration.
 """
 
-import datetime
 import io
 import sys
+from datetime import datetime, timedelta, timezone
 from urllib.request import urlopen
 
-import pytz
 import transaction
 from zope.component import getUtility
 
@@ -19,11 +18,11 @@ from lp.services.librarian.interfaces import ILibraryFileAliasSet
 
 FILE_SIZE = 1024
 FILE_DATA = b"x" * FILE_SIZE
-FILE_LIFETIME = datetime.timedelta(hours=1)
+FILE_LIFETIME = timedelta(hours=1)
 
 
 def store_file(client):
-    expiry_date = datetime.datetime.now(pytz.UTC) + FILE_LIFETIME
+    expiry_date = datetime.now(timezone.utc) + FILE_LIFETIME
     file_id = client.addFile(
         "smoke-test-file",
         FILE_SIZE,
diff --git a/lib/lp/services/librarianserver/librariangc.py b/lib/lp/services/librarianserver/librariangc.py
index a0ec1ad..a810c5f 100644
--- a/lib/lp/services/librarianserver/librariangc.py
+++ b/lib/lp/services/librarianserver/librariangc.py
@@ -9,11 +9,10 @@ import os
 import re
 import sys
 from contextlib import ExitStack
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from time import time
 
 import iso8601
-import pytz
 from swiftclient import client as swiftclient
 from zope.interface import implementer
 
@@ -62,7 +61,7 @@ def file_exists(content_id):
 
 def _utcnow():
     # Wrapper that is replaced in the test suite.
-    return datetime.now(pytz.UTC)
+    return datetime.now(timezone.utc)
 
 
 def open_stream(content_id):
@@ -118,7 +117,7 @@ def confirm_no_clock_skew(con):
     cur = con.cursor()
     try:
         cur.execute("SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC'")
-        db_now = cur.fetchone()[0].replace(tzinfo=pytz.UTC)
+        db_now = cur.fetchone()[0].replace(tzinfo=timezone.utc)
     finally:
         cur.close()
     local_now = _utcnow()
diff --git a/lib/lp/services/librarianserver/libraryprotocol.py b/lib/lp/services/librarianserver/libraryprotocol.py
index 625ed35..2b63189 100644
--- a/lib/lp/services/librarianserver/libraryprotocol.py
+++ b/lib/lp/services/librarianserver/libraryprotocol.py
@@ -1,9 +1,8 @@
 # Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-from pytz import utc
 from twisted.internet import protocol
 from twisted.internet.threads import deferToThread
 from twisted.protocols import basic
@@ -194,7 +193,7 @@ class FileUploadProtocol(basic.LineReceiver):
             raise ProtocolViolation("Invalid File-Expires: " + value)
 
         self.newFile.expires = datetime.fromtimestamp(epoch).replace(
-            tzinfo=utc
+            tzinfo=timezone.utc
         )
 
     def header_database_name(self, value):
diff --git a/lib/lp/services/librarianserver/tests/test_gc.py b/lib/lp/services/librarianserver/tests/test_gc.py
index bbb3ed4..41087cc 100644
--- a/lib/lp/services/librarianserver/tests/test_gc.py
+++ b/lib/lp/services/librarianserver/tests/test_gc.py
@@ -10,11 +10,10 @@ import os
 import shutil
 import tempfile
 from contextlib import contextmanager
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from subprocess import PIPE, STDOUT, Popen
 from urllib.parse import urljoin
 
-import pytz
 import requests
 import transaction
 from fixtures import MockPatchObject
@@ -452,7 +451,7 @@ class TestLibrarianGarbageCollectionBase:
             return org_time() + 24 * 60 * 60 + 1
 
         def tomorrow_utcnow():
-            return datetime.now(pytz.UTC) + timedelta(days=1, seconds=1)
+            return datetime.now(timezone.utc) + timedelta(days=1, seconds=1)
 
         try:
             librariangc.time = tomorrow_time
diff --git a/lib/lp/services/librarianserver/tests/test_web.py b/lib/lp/services/librarianserver/tests/test_web.py
index 8eda8fd..5747f9e 100644
--- a/lib/lp/services/librarianserver/tests/test_web.py
+++ b/lib/lp/services/librarianserver/tests/test_web.py
@@ -5,12 +5,11 @@ import hashlib
 import http.client
 import os
 import unittest
-from datetime import datetime
+from datetime import datetime, timezone
 from gzip import GzipFile
 from io import BytesIO
 from urllib.parse import urlparse
 
-import pytz
 import requests
 import transaction
 from lazr.uri import URI
@@ -296,7 +295,7 @@ class LibrarianWebTestCase(LibrarianWebTestMixin, TestCaseWithFactory):
             LibraryFileAlias, file_alias_id
         )
         file_alias.date_created = datetime(
-            2001, 1, 30, 13, 45, 59, tzinfo=pytz.utc
+            2001, 1, 30, 13, 45, 59, tzinfo=timezone.utc
         )
 
         # Commit so the file is available from the Librarian.
@@ -477,7 +476,7 @@ class LibrarianWebTestCase(LibrarianWebTestMixin, TestCaseWithFactory):
             LibraryFileAlias, fileAlias
         )
         file_alias.date_created = datetime(
-            2001, 1, 30, 13, 45, 59, tzinfo=pytz.utc
+            2001, 1, 30, 13, 45, 59, tzinfo=timezone.utc
         )
         # Commit the update.
         self.commit()
diff --git a/lib/lp/services/messages/doc/message.rst b/lib/lp/services/messages/doc/message.rst
index 8a643be..4dfcfcb 100644
--- a/lib/lp/services/messages/doc/message.rst
+++ b/lib/lp/services/messages/doc/message.rst
@@ -497,8 +497,7 @@ parameter to fromEmail(). This is optional, and defaults to None, but it
 allows us to deal with those situations where fromEmail() would
 otherwise reject the method as invalid.
 
-    >>> from datetime import datetime
-    >>> import pytz
+    >>> from datetime import datetime, timezone
 
     >>> msg_bytes = b"""\
     ... From: foo.bar@xxxxxxxxxxxxx
@@ -508,13 +507,11 @@ otherwise reject the method as invalid.
     ... In search of cheesy comestibles.
     ... """
 
-    >>> date_created = datetime(
-    ...     2008, 7, 9, 14, 27, 40, tzinfo=pytz.timezone("UTC")
-    ... )
+    >>> date_created = datetime(2008, 7, 9, 14, 27, 40, tzinfo=timezone.utc)
     >>> msg = msgset.fromEmail(msg_bytes, date_created=date_created)
 
     >>> msg.datecreated
-    datetime.datetime(2008, 7, 9, 14, 27, 40, tzinfo=<UTC>)
+    datetime.datetime(2008, 7, 9, 14, 27, 40, tzinfo=datetime.timezone.utc)
 
 But, we make sure that we don't create a message with a date that is
 futuristic:
@@ -529,7 +526,7 @@ futuristic:
     ... Moo
     ... """
     ... )
-    >>> msg.datecreated > datetime.now(tz=pytz.timezone("UTC"))
+    >>> msg.datecreated > datetime.now(tz=timezone.utc)
     False
 
 And similarly, we will consider any message that claims to be older than
@@ -545,7 +542,7 @@ And similarly, we will consider any message that claims to be older than
     ... Moo
     ... """
     ... )
-    >>> thedistantpast = datetime(1990, 1, 1, tzinfo=pytz.timezone("UTC"))
+    >>> thedistantpast = datetime(1990, 1, 1, tzinfo=timezone.utc)
     >>> msg.datecreated < thedistantpast
     False
 
diff --git a/lib/lp/services/messages/model/message.py b/lib/lp/services/messages/model/message.py
index 21f8bc9..c6eb5b0 100644
--- a/lib/lp/services/messages/model/message.py
+++ b/lib/lp/services/messages/model/message.py
@@ -12,13 +12,12 @@ __all__ = [
 import email
 import logging
 import os.path
-from datetime import datetime
+from datetime import datetime, timezone
 from email.header import decode_header, make_header
 from email.utils import make_msgid, mktime_tz, parseaddr, parsedate_tz
 from io import BytesIO
 from operator import attrgetter
 
-import pytz
 import six
 from lazr.config import as_timedelta
 from storm.locals import (
@@ -83,7 +82,7 @@ def utcdatetime_from_field(field_value):
     try:
         date_tuple = parsedate_tz(field_value)
         timestamp = mktime_tz(date_tuple)
-        return datetime.fromtimestamp(timestamp, tz=pytz.timezone("UTC"))
+        return datetime.fromtimestamp(timestamp, tz=timezone.utc)
     except (TypeError, ValueError, OverflowError):
         raise InvalidEmailMessage("Invalid date %s" % field_value)
 
@@ -481,8 +480,8 @@ class MessageSet:
 
         # Make sure we don't create an email with a datecreated in the
         # distant past or future.
-        now = datetime.now(pytz.timezone("UTC"))
-        thedistantpast = datetime(1990, 1, 1, tzinfo=pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
+        thedistantpast = datetime(1990, 1, 1, tzinfo=timezone.utc)
         if datecreated < thedistantpast or datecreated > now:
             datecreated = UTC_NOW
 
@@ -764,7 +763,7 @@ class DirectEmailAuthorization:
         # Users are only allowed to send X number of messages in a certain
         # period of time.  Both the number of messages and the time period
         # are configurable.
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         after = now - as_timedelta(
             config.launchpad.user_to_user_throttle_interval
         )
@@ -773,7 +772,7 @@ class DirectEmailAuthorization:
     @property
     def throttle_date(self):
         """See `IDirectEmailAuthorization`."""
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         after = now - as_timedelta(
             config.launchpad.user_to_user_throttle_interval
         )
diff --git a/lib/lp/services/messages/model/messagerevision.py b/lib/lp/services/messages/model/messagerevision.py
index 50e9980..b2fcf4a 100644
--- a/lib/lp/services/messages/model/messagerevision.py
+++ b/lib/lp/services/messages/model/messagerevision.py
@@ -8,7 +8,8 @@ __all__ = [
     "MessageRevisionChunk",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.interface import implementer
 
@@ -37,10 +38,10 @@ class MessageRevision(StormBase):
     revision = Int(name="revision", allow_none=False)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False
+        name="date_created", tzinfo=timezone.utc, allow_none=False
     )
     date_deleted = DateTime(
-        name="date_deleted", tzinfo=pytz.UTC, allow_none=True
+        name="date_deleted", tzinfo=timezone.utc, allow_none=True
     )
 
     def __init__(self, message, revision, date_created, date_deleted=None):
diff --git a/lib/lp/services/oauth/browser/__init__.py b/lib/lp/services/oauth/browser/__init__.py
index 475bf53..40fdba7 100644
--- a/lib/lp/services/oauth/browser/__init__.py
+++ b/lib/lp/services/oauth/browser/__init__.py
@@ -9,9 +9,8 @@ __all__ = [
 ]
 
 import json
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from lazr.restful import HTTPResource
 from zope.component import getUtility
 from zope.formlib.form import Action, Actions, expandPrefix
@@ -395,9 +394,7 @@ class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
         duration_seconds = TemporaryIntegrations.DURATION.get(duration)
         if duration_seconds is not None:
             duration_delta = timedelta(seconds=duration_seconds)
-            expiration_date = (
-                datetime.now(pytz.timezone("UTC")) + duration_delta
-            )
+            expiration_date = datetime.now(timezone.utc) + duration_delta
         else:
             expiration_date = None
         try:
diff --git a/lib/lp/services/oauth/doc/oauth-pages.rst b/lib/lp/services/oauth/doc/oauth-pages.rst
index 47bb34d..ee11f7a 100644
--- a/lib/lp/services/oauth/doc/oauth-pages.rst
+++ b/lib/lp/services/oauth/doc/oauth-pages.rst
@@ -46,7 +46,7 @@ consumer's request to access Launchpad on their behalf.
 When the client doesn't specify a duration, the resulting request
 token will have no expiration date set.
 
-    >>> from datetime import datetime
+    >>> from datetime import datetime, timezone
     >>> view, token = get_view_with_fresh_token({})
     >>> view.reviewToken(OAuthPermission.READ_PRIVATE, None)
     >>> print(token.date_expires)
@@ -55,13 +55,12 @@ token will have no expiration date set.
 When the client specifies a duration, the resulting request
 token will have an appropriate expiration date set.
 
-    >>> import pytz
     >>> from lp.services.oauth.browser import TemporaryIntegrations
     >>> view, token = get_view_with_fresh_token({})
     >>> view.reviewToken(
     ...     OAuthPermission.READ_PRIVATE, TemporaryIntegrations.HOUR
     ... )
-    >>> token.date_expires > datetime.now(pytz.timezone("UTC"))
+    >>> token.date_expires > datetime.now(timezone.utc)
     True
 
 When the consumer doesn't specify a context, the token will not have a
diff --git a/lib/lp/services/oauth/model.py b/lib/lp/services/oauth/model.py
index 9736101..94f1d5e 100644
--- a/lib/lp/services/oauth/model.py
+++ b/lib/lp/services/oauth/model.py
@@ -12,9 +12,8 @@ __all__ = [
 
 import hashlib
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from storm.locals import Bool, DateTime, Int, Reference, Unicode
 from zope.interface import implementer
 
@@ -73,7 +72,9 @@ class OAuthConsumer(OAuthBase, StormBase):
     __storm_table__ = "OAuthConsumer"
 
     id = Int(primary=True)
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
     disabled = Bool(allow_none=False, default=False)
     key = Unicode(allow_none=False)
     _secret = Unicode(name="secret", allow_none=True, default="")
@@ -209,8 +210,10 @@ class OAuthAccessToken(OAuthBase, StormBase):
     consumer = Reference(consumer_id, "OAuthConsumer.id")
     person_id = Int(name="person", allow_none=False)
     person = Reference(person_id, "Person.id")
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
-    date_expires = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    date_expires = DateTime(tzinfo=timezone.utc, allow_none=True, default=None)
     key = Unicode(allow_none=False)
     _secret = Unicode(name="secret", allow_none=True, default="")
 
@@ -271,7 +274,7 @@ class OAuthAccessToken(OAuthBase, StormBase):
 
     @property
     def is_expired(self):
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         return self.date_expires is not None and self.date_expires <= now
 
     def isSecretValid(self, secret):
@@ -290,13 +293,17 @@ class OAuthRequestToken(OAuthBase, StormBase):
     consumer = Reference(consumer_id, "OAuthConsumer.id")
     person_id = Int(name="person", allow_none=True, default=None)
     person = Reference(person_id, "Person.id")
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
-    date_expires = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=UTC_NOW
+    )
+    date_expires = DateTime(tzinfo=timezone.utc, allow_none=True, default=None)
     key = Unicode(allow_none=False)
     _secret = Unicode(name="secret", allow_none=True, default="")
 
     permission = DBEnum(enum=OAuthPermission, allow_none=True, default=None)
-    date_reviewed = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+    date_reviewed = DateTime(
+        tzinfo=timezone.utc, allow_none=True, default=None
+    )
 
     product_id = Int(name="product", allow_none=True, default=None)
     product = Reference(product_id, "Product.id")
@@ -353,7 +360,7 @@ class OAuthRequestToken(OAuthBase, StormBase):
 
     @property
     def is_expired(self):
-        now = datetime.now(pytz.UTC)
+        now = datetime.now(timezone.utc)
         expires = self.date_created + timedelta(hours=REQUEST_TOKEN_VALIDITY)
         return expires <= now
 
@@ -372,7 +379,7 @@ class OAuthRequestToken(OAuthBase, StormBase):
                 "This request token has expired and can no longer be "
                 "reviewed."
             )
-        self.date_reviewed = datetime.now(pytz.UTC)
+        self.date_reviewed = datetime.now(timezone.utc)
         self.date_expires = date_expires
         self.person = user
         self.permission = permission
diff --git a/lib/lp/services/oauth/stories/authorize-token.rst b/lib/lp/services/oauth/stories/authorize-token.rst
index 39ce2bc..61f3cc5 100644
--- a/lib/lp/services/oauth/stories/authorize-token.rst
+++ b/lib/lp/services/oauth/stories/authorize-token.rst
@@ -304,10 +304,9 @@ the success message is printed.
 If the token has expired, we notify the user, and inhibit the callback.
 
     >>> token = request_token_for(consumer)
-    >>> from datetime import datetime, timedelta
-    >>> from pytz import UTC
+    >>> from datetime import datetime, timedelta, timezone
     >>> from zope.security.proxy import removeSecurityProxy
-    >>> date_created = datetime.now(UTC) - timedelta(hours=3)
+    >>> date_created = datetime.now(timezone.utc) - timedelta(hours=3)
     >>> removeSecurityProxy(token).date_created = date_created
     >>> params = dict(
     ...     oauth_token=token.key, oauth_callback="http://example.com/oauth";
diff --git a/lib/lp/services/oauth/tests/test_tokens.py b/lib/lp/services/oauth/tests/test_tokens.py
index b0c3059..9fb405d 100644
--- a/lib/lp/services/oauth/tests/test_tokens.py
+++ b/lib/lp/services/oauth/tests/test_tokens.py
@@ -8,9 +8,8 @@ OAuth specification is defined in <http://oauth.net/core/1.0/>.
 """
 
 import hashlib
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
-import pytz
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
@@ -45,7 +44,7 @@ class TestOAuth(TestCaseWithFactory):
         self.person = self.factory.makePerson()
         self.consumer = self.factory.makeOAuthConsumer()
 
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         self.in_a_while = now + timedelta(hours=1)
         self.a_long_time_ago = now - timedelta(hours=1000)
 
@@ -119,7 +118,7 @@ class TestRequestTokens(TestOAuth):
 
     def test_date_created(self):
         request_token, _ = self.consumer.newRequestToken()
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
         self.assertTrue(request_token.date_created <= now)
 
     def test_new_token_is_not_reviewed(self):
@@ -156,7 +155,7 @@ class TestRequestTokens(TestOAuth):
         request_token, _ = self.consumer.newRequestToken()
 
         request_token.review(self.person, OAuthPermission.WRITE_PUBLIC)
-        now = datetime.now(pytz.timezone("UTC"))
+        now = datetime.now(timezone.utc)
 
         self.assertTrue(request_token.is_reviewed)
         self.assertEqual(request_token.person, self.person)
diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py
index 24018e0..6184599 100644
--- a/lib/lp/services/scripts/base.py
+++ b/lib/lp/services/scripts/base.py
@@ -10,7 +10,6 @@ __all__ = [
     "SilentLaunchpadScriptFailure",
 ]
 
-import datetime
 import io
 import logging
 import os.path
@@ -18,11 +17,11 @@ import sys
 from configparser import ConfigParser
 from contextlib import contextmanager
 from cProfile import Profile
+from datetime import datetime, timedelta, timezone
 from optparse import OptionParser
 from typing import Optional
 from urllib.parse import urlparse, urlunparse
 
-import pytz
 import requests
 import transaction
 from contrib.glock import GlobalLock, LockAlreadyAcquired
@@ -45,7 +44,6 @@ from lp.services.webapp.errorlog import globalErrorUtility
 from lp.services.webapp.interaction import ANONYMOUS, setupInteractionByEmail
 
 LOCK_PATH = "/var/lock/"
-UTC = pytz.UTC
 
 
 class LaunchpadScriptFailure(Exception):
@@ -309,7 +307,7 @@ class LaunchpadScript:
         # Should be called directly by scripts that actually need it.
         set_immediate_mail_delivery(True)
 
-        date_started = datetime.datetime.now(UTC)
+        date_started = datetime.now(timezone.utc)
         profiler = None
         if self.options.profile:
             profiler = Profile()
@@ -327,7 +325,7 @@ class LaunchpadScript:
         except SilentLaunchpadScriptFailure as e:
             sys.exit(e.exit_status)
         else:
-            date_completed = datetime.datetime.now(UTC)
+            date_completed = datetime.now(timezone.utc)
             self.record_activity(date_started, date_completed)
         finally:
             install_feature_controller(original_feature_controller)
@@ -418,7 +416,7 @@ class LaunchpadCronScript(LaunchpadScript):
                 # can be distinguished from real completions.  Avoid
                 # touching the database here, since that could be
                 # problematic during schema updates.
-                emit_script_activity_metric(self.name, datetime.timedelta(0))
+                emit_script_activity_metric(self.name, timedelta(0))
                 sys.exit(0)
 
         super()._init_db(isolation)
diff --git a/lib/lp/services/scripts/doc/script-monitoring.rst b/lib/lp/services/scripts/doc/script-monitoring.rst
index a824741..9d245fb 100644
--- a/lib/lp/services/scripts/doc/script-monitoring.rst
+++ b/lib/lp/services/scripts/doc/script-monitoring.rst
@@ -17,13 +17,12 @@ When a script completes successfully, it should record the fact in the
 database.  This is performed with a call to
 IScriptActivitySet.recordSuccess():
 
-    >>> import datetime
+    >>> from datetime import datetime, timezone
     >>> import socket
     >>> from textwrap import dedent
     >>> from unittest import mock
 
     >>> from fixtures import MockPatchObject
-    >>> import pytz
     >>> from zope.component import getUtility
 
     >>> from lp.services.config import config
@@ -33,7 +32,6 @@ IScriptActivitySet.recordSuccess():
     >>> from lp.services.statsd.interfaces.statsd_client import IStatsdClient
     >>> from lp.testing.dbuser import switch_dbuser
 
-    >>> UTC = pytz.timezone("UTC")
     >>> switch_dbuser("garbo_daily")  # A script db user
 
     >>> config.push(
@@ -51,9 +49,9 @@ IScriptActivitySet.recordSuccess():
     >>> with MockPatchObject(statsd_client, "_client", stats_client):
     ...     activity = getUtility(IScriptActivitySet).recordSuccess(
     ...         name="script-name",
-    ...         date_started=datetime.datetime(2007, 2, 1, 10, 0, tzinfo=UTC),
-    ...         date_completed=datetime.datetime(
-    ...             2007, 2, 1, 10, 1, tzinfo=UTC
+    ...         date_started=datetime(2007, 2, 1, 10, 0, tzinfo=timezone.utc),
+    ...         date_completed=datetime(
+    ...             2007, 2, 1, 10, 1, tzinfo=timezone.utc
     ...         ),
     ...         hostname="script-host",
     ...     )
@@ -104,8 +102,8 @@ script ran on, as determined by 'socket.gethostname()':
 
     >>> local_activity = getUtility(IScriptActivitySet).recordSuccess(
     ...     name=factory.getUniqueString(),
-    ...     date_started=datetime.datetime.now(UTC),
-    ...     date_completed=datetime.datetime.now(UTC),
+    ...     date_started=datetime.now(timezone.utc),
+    ...     date_completed=datetime.now(timezone.utc),
     ... )
     >>> local_activity.hostname == socket.gethostname()
     True
diff --git a/lib/lp/services/scripts/model/scriptactivity.py b/lib/lp/services/scripts/model/scriptactivity.py
index 3699d2a..87602e7 100644
--- a/lib/lp/services/scripts/model/scriptactivity.py
+++ b/lib/lp/services/scripts/model/scriptactivity.py
@@ -7,8 +7,8 @@ __all__ = [
 ]
 
 import socket
+from datetime import timezone
 
-import pytz
 import six
 from storm.locals import DateTime, Int, Unicode
 from zope.interface import implementer
@@ -30,8 +30,8 @@ class ScriptActivity(StormBase):
     id = Int(primary=True)
     name = Unicode(allow_none=False)
     hostname = Unicode(allow_none=False)
-    date_started = DateTime(tzinfo=pytz.UTC, allow_none=False)
-    date_completed = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    date_started = DateTime(tzinfo=timezone.utc, allow_none=False)
+    date_completed = DateTime(tzinfo=timezone.utc, allow_none=False)
 
     def __init__(self, name, hostname, date_started, date_completed):
         super().__init__()
diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py
index 868b634..126938a 100644
--- a/lib/lp/services/signing/model/signingkey.py
+++ b/lib/lp/services/signing/model/signingkey.py
@@ -9,7 +9,8 @@ __all__ = [
     "SigningKey",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import Bytes, DateTime, Int, Reference, Unicode
 from zope.component import getUtility
 from zope.interface import implementer, provider
@@ -47,7 +48,9 @@ class SigningKey(StormBase):
 
     public_key = Bytes(allow_none=False)
 
-    date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    date_created = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     def __init__(
         self,
diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py
index 74c5d5b..7b1d985 100644
--- a/lib/lp/services/signing/tests/test_signingkey.py
+++ b/lib/lp/services/signing/tests/test_signingkey.py
@@ -2,12 +2,11 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import base64
-from datetime import datetime
+from datetime import datetime, timezone
 
 import responses
 from fixtures.testcase import TestWithFixtures
 from nacl.public import PrivateKey
-from pytz import utc
 from storm.store import Store
 from testtools.matchers import (
     AfterPreprocessing,
@@ -98,7 +97,7 @@ class TestSigningKey(TestCaseWithFactory, TestWithFixtures):
 
         priv_key = PrivateKey.generate()
         pub_key = priv_key.public_key
-        created_at = datetime(2020, 4, 16, 16, 35).replace(tzinfo=utc)
+        created_at = datetime(2020, 4, 16, 16, 35).replace(tzinfo=timezone.utc)
 
         key = SigningKey.inject(
             SigningKeyType.KMOD,
@@ -130,7 +129,7 @@ class TestSigningKey(TestCaseWithFactory, TestWithFixtures):
 
         priv_key = PrivateKey.generate()
         pub_key = priv_key.public_key
-        created_at = datetime(2020, 4, 16, 16, 35).replace(tzinfo=utc)
+        created_at = datetime(2020, 4, 16, 16, 35).replace(tzinfo=timezone.utc)
 
         key = SigningKey.inject(
             SigningKeyType.KMOD,
@@ -374,7 +373,7 @@ class TestArchiveSigningKey(TestCaseWithFactory):
         priv_key = PrivateKey.generate()
         pub_key = priv_key.public_key
 
-        now = datetime.now().replace(tzinfo=utc)
+        now = datetime.now().replace(tzinfo=timezone.utc)
         arch_key = getUtility(IArchiveSigningKeySet).inject(
             SigningKeyType.UEFI,
             bytes(priv_key),
@@ -421,7 +420,7 @@ class TestArchiveSigningKey(TestCaseWithFactory):
         priv_key = PrivateKey.generate()
         pub_key = priv_key.public_key
 
-        now = datetime.now().replace(tzinfo=utc)
+        now = datetime.now().replace(tzinfo=timezone.utc)
         arch_key = getUtility(IArchiveSigningKeySet).inject(
             SigningKeyType.UEFI,
             bytes(priv_key),
diff --git a/lib/lp/services/statistics/model/statistics.py b/lib/lp/services/statistics/model/statistics.py
index f256d89..70a5ce9 100644
--- a/lib/lp/services/statistics/model/statistics.py
+++ b/lib/lp/services/statistics/model/statistics.py
@@ -8,7 +8,8 @@ __all__ = [
     "LaunchpadStatisticSet",
 ]
 
-import pytz
+from datetime import timezone
+
 from storm.locals import DateTime, Int, Unicode
 from zope.component import getUtility
 from zope.interface import implementer
@@ -48,7 +49,9 @@ class LaunchpadStatistic(StormBase):
 
     name = Unicode(allow_none=False)
     value = Int(allow_none=False)
-    dateupdated = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
+    dateupdated = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
+    )
 
     def __init__(self, name, value):
         super().__init__()
diff --git a/lib/lp/services/statsd/numbercruncher.py b/lib/lp/services/statsd/numbercruncher.py
index b18adf5..bd8cd3b 100644
--- a/lib/lp/services/statsd/numbercruncher.py
+++ b/lib/lp/services/statsd/numbercruncher.py
@@ -6,9 +6,8 @@
 __all__ = ["NumberCruncher"]
 
 import logging
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from storm.expr import Count, Sum
 from twisted.application import service
@@ -184,7 +183,8 @@ class NumberCruncher(service.Service):
                 CodeImportJob.state == CodeImportJobState.PENDING,
             ).count()
             overdue = store.find(
-                CodeImportJob, CodeImportJob.date_due < datetime.now(pytz.UTC)
+                CodeImportJob,
+                CodeImportJob.date_due < datetime.now(timezone.utc),
             ).count()
             self._sendGauge("codeimport.pending", pending)
             self._sendGauge("codeimport.overdue", overdue)
diff --git a/lib/lp/services/statsd/tests/test_numbercruncher.py b/lib/lp/services/statsd/tests/test_numbercruncher.py
index d3ed3fe..a7d4dd7 100644
--- a/lib/lp/services/statsd/tests/test_numbercruncher.py
+++ b/lib/lp/services/statsd/tests/test_numbercruncher.py
@@ -3,9 +3,8 @@
 
 """Tests for the stats number cruncher daemon."""
 
-from datetime import datetime
+from datetime import datetime, timezone
 
-import pytz
 import transaction
 from storm.store import Store
 from testtools.matchers import Equals, MatchesListwise, MatchesSetwise, Not
@@ -399,7 +398,7 @@ class TestNumberCruncher(StatsMixin, TestCaseWithFactory):
             archive=archive, status=PackagePublishingStatus.PUBLISHED
         )
         bpph.binarypackagerelease.build.updateStatus(
-            BuildStatus.BUILDING, date_started=datetime.now(pytz.UTC)
+            BuildStatus.BUILDING, date_started=datetime.now(timezone.utc)
         )
         bpph.binarypackagerelease.build.updateStatus(BuildStatus.FULLYBUILT)
         clock = task.Clock()
diff --git a/lib/lp/services/temporaryblobstorage/model.py b/lib/lp/services/temporaryblobstorage/model.py
index 0d3bb3a..af8060c 100644
--- a/lib/lp/services/temporaryblobstorage/model.py
+++ b/lib/lp/services/temporaryblobstorage/model.py
@@ -8,10 +8,9 @@ __all__ = [
 
 
 import uuid
-from datetime import timedelta
+from datetime import timedelta, timezone
 from io import BytesIO
 
-import pytz
 from storm.locals import DateTime, Int, Reference, Unicode
 from zope.component import getUtility
 from zope.interface import implementer
@@ -42,7 +41,9 @@ class TemporaryBlobStorage(StormBase):
     uuid = Unicode(allow_none=False)
     file_alias_id = Int(name="file_alias", allow_none=False)
     file_alias = Reference(file_alias_id, "LibraryFileAlias.id")
-    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    date_created = DateTime(
+        tzinfo=timezone.utc, allow_none=False, default=DEFAULT
+    )
 
     def __init__(self, uuid, file_alias):
         super().__init__()
diff --git a/lib/lp/services/tests/test_command_spawner.py b/lib/lp/services/tests/test_command_spawner.py
index 87d2904..4c5df5e 100644
--- a/lib/lp/services/tests/test_command_spawner.py
+++ b/lib/lp/services/tests/test_command_spawner.py
@@ -3,11 +3,10 @@
 
 """Tests for `CommandSpawner`."""
 
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from fcntl import F_GETFL, fcntl
 from os import O_NONBLOCK, fdopen, pipe
 
-from pytz import utc
 from testtools.matchers import LessThan
 
 from lp.services.command_spawner import (
@@ -243,28 +242,28 @@ class TestCommandSpawnerAcceptance(TestCase):
 
     def test_communicate_returns_after_event(self):
         spawner = self._makeSpawner()
-        before = datetime.now(utc)
+        before = datetime.now(timezone.utc)
         spawner.start(["/bin/sleep", "10"])
         spawner.start("/bin/pwd")
         spawner.communicate()
-        after = datetime.now(utc)
+        after = datetime.now(timezone.utc)
         self.assertThat(after - before, LessThan(timedelta(seconds=10)))
 
     def test_kill_terminates_processes(self):
         spawner = self._makeSpawner()
         spawner.start(["/bin/sleep", "10"])
         spawner.start(["/bin/sleep", "10"])
-        before = datetime.now(utc)
+        before = datetime.now(timezone.utc)
         spawner.kill()
         spawner.complete()
-        after = datetime.now(utc)
+        after = datetime.now(timezone.utc)
         self.assertThat(after - before, LessThan(timedelta(seconds=10)))
 
     def test_start_does_not_block(self):
         spawner = self._makeSpawner()
-        before = datetime.now(utc)
+        before = datetime.now(timezone.utc)
         spawner.start(["/bin/sleep", "10"])
-        after = datetime.now(utc)
+        after = datetime.now(timezone.utc)
         self.assertThat(after - before, LessThan(timedelta(seconds=10)))
 
     def test_subprocesses_run_in_parallel(self):
@@ -275,9 +274,9 @@ class TestCommandSpawnerAcceptance(TestCase):
         for counter in range(processes):
             spawner.start(["/bin/sleep", str(seconds)])
 
-        before = datetime.now(utc)
+        before = datetime.now(timezone.utc)
         spawner.complete()
-        after = datetime.now(utc)
+        after = datetime.now(timezone.utc)
 
         sequential_time = timedelta(seconds=(seconds * processes))
         self.assertThat(after - before, LessThan(sequential_time))
diff --git a/lib/lp/services/tests/test_utils.py b/lib/lp/services/tests/test_utils.py
index 3483ae0..564ce5d 100644
--- a/lib/lp/services/tests/test_utils.py
+++ b/lib/lp/services/tests/test_utils.py
@@ -6,11 +6,10 @@
 import itertools
 import os
 from contextlib import contextmanager
-from datetime import datetime
+from datetime import datetime, timezone
 from functools import partial
 
 from fixtures import TempDir
-from pytz import UTC
 from testtools.matchers import Equals, GreaterThan, LessThan, MatchesAny
 
 from lp.services.utils import (
@@ -318,15 +317,15 @@ class TestUTCNow(TestCase):
         # utc_now() returns a timezone-aware timestamp with the timezone of
         # UTC.
         now = u