← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~andrey-fedoseev/launchpad:mypy into launchpad:master

 

Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:mypy into launchpad:master.

Commit message:
Prepare some packages for type checking with `mypy`:

- lp.answers
- lp.app
- lp.archivepublisher
- lp.archiveuploader
- lp.blueprint
- lp.bugs

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/425377
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:mypy into launchpad:master.
diff --git a/lib/lp/answers/adapters.py b/lib/lp/answers/adapters.py
index 3c7f037..17e0f81 100644
--- a/lib/lp/answers/adapters.py
+++ b/lib/lp/answers/adapters.py
@@ -3,9 +3,6 @@
 
 """Adapters used in the Answer Tracker."""
 
-__all__ = []
-
-
 from lp.answers.interfaces.faqtarget import IFAQTarget
 
 
diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py
index f00b3b2..bf05b47 100644
--- a/lib/lp/answers/browser/faqcollection.py
+++ b/lib/lp/answers/browser/faqcollection.py
@@ -8,8 +8,11 @@ __all__ = [
     "SearchFAQsView",
 ]
 
+from typing import Type
 from urllib.parse import urlencode
 
+from zope.interface import Interface
+
 from lp import _
 from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH, QuestionSort
 from lp.answers.interfaces.faqcollection import (
@@ -28,7 +31,7 @@ from lp.services.webapp.menu import enabled_with_permission
 class FAQCollectionMenu(NavigationMenu):
     """Base menu definition for `IFAQCollection`."""
 
-    usedfor = IFAQCollection
+    usedfor = IFAQCollection  # type: Type[Interface]
     facet = "answers"
     links = ["list_all", "create_faq"]
 
@@ -82,7 +85,9 @@ class SearchFAQsView(LaunchpadFormView):
         else:
             return _("FAQs for $displayname", mapping=replacements)
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     @property
     def empty_listing_message(self):
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index 32d63dc..485df06 100644
--- a/lib/lp/answers/browser/question.py
+++ b/lib/lp/answers/browser/question.py
@@ -801,7 +801,9 @@ class QuestionChangeStatusView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class QuestionTargetWidget(LaunchpadTargetWidget):
@@ -820,7 +822,6 @@ class QuestionEditView(LaunchpadEditFormView):
     """View for editing a Question."""
 
     schema = IQuestion
-    label = "Edit question"
     field_names = [
         "language",
         "title",
@@ -838,7 +839,9 @@ class QuestionEditView(LaunchpadEditFormView):
     def page_title(self):
         return "Edit question #%s details" % self.context.id
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     def setUpFields(self):
         """Select the subset of fields to display.
@@ -874,7 +877,9 @@ class QuestionEditView(LaunchpadEditFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class QuestionRejectView(LaunchpadFormView):
@@ -921,7 +926,9 @@ class QuestionRejectView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class LinkFAQMixin:
@@ -1587,4 +1594,6 @@ class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
index 488c860..4ee9263 100644
--- a/lib/lp/answers/browser/questiontarget.py
+++ b/lib/lp/answers/browser/questiontarget.py
@@ -231,7 +231,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
             else:
                 return _("Questions for ${context}", mapping=replacements)
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     @property
     def display_target_column(self):
@@ -562,7 +564,9 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
                 mapping={"context": self.context.displayname},
             )
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     @property
     def empty_listing_message(self):
@@ -615,7 +619,9 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
                 mapping={"context": self.context.displayname},
             )
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     @property
     def empty_listing_message(self):
@@ -691,7 +697,9 @@ class QuestionCollectionByLanguageView(SearchQuestionsView):
         else:
             return _("${language} questions in ${context}", mapping=mapping)
 
-    label = page_title
+    @property
+    def label(self):
+        return self.page_title
 
     @property
     def empty_listing_message(self):
diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py
index 0619fc3..b901faf 100644
--- a/lib/lp/answers/browser/tests/test_question.py
+++ b/lib/lp/answers/browser/tests/test_question.py
@@ -3,8 +3,6 @@
 
 """Tests for the question module."""
 
-__all__ = []
-
 from zope.security.proxy import removeSecurityProxy
 
 from lp.answers.browser.question import QuestionTargetWidget
diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py
index 0fd7031..c01790a 100644
--- a/lib/lp/answers/browser/tests/test_views.py
+++ b/lib/lp/answers/browser/tests/test_views.py
@@ -3,8 +3,6 @@
 
 """Test harness for Answer Tracker related unit tests."""
 
-__all__ = []
-
 import unittest
 
 from lp.testing import BrowserTestCase
diff --git a/lib/lp/answers/mail/__init__.py b/lib/lp/answers/mail/__init__.py
index 6b3f0d4..516532e 100644
--- a/lib/lp/answers/mail/__init__.py
+++ b/lib/lp/answers/mail/__init__.py
@@ -1,4 +1,2 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
-
-__all__ = []
diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
index 186c7a4..1d8e670 100644
--- a/lib/lp/answers/model/question.py
+++ b/lib/lp/answers/model/question.py
@@ -270,6 +270,7 @@ class Question(StormBase, BugLinkTargetMixin):
         return list(self._messages)
 
     # attributes
+    @property
     def target(self):
         """See `IQuestion`."""
         if self.product:
@@ -279,7 +280,8 @@ class Question(StormBase, BugLinkTargetMixin):
         else:
             return self.distribution
 
-    def _settarget(self, question_target):
+    @target.setter
+    def target(self, question_target):
         """See Question.target."""
         if not IQuestionTarget.providedBy(question_target):
             raise QuestionTargetError("The target must be an IQuestionTarget")
@@ -300,8 +302,6 @@ class Question(StormBase, BugLinkTargetMixin):
                 "Unknown IQuestionTarget type of %s" % question_target
             )
 
-    target = property(target, _settarget, doc=target.__doc__)
-
     @property
     def followup_subject(self):
         """See `IMessageTarget`."""
diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py
index d31d8aa..809170d 100644
--- a/lib/lp/answers/tests/test_question_workflow.py
+++ b/lib/lp/answers/tests/test_question_workflow.py
@@ -9,8 +9,6 @@ but testing all the possible transitions makes the documentation more heavy
 than necessary. This is tested here.
 """
 
-__all__ = []
-
 import traceback
 from datetime import datetime, timedelta
 
diff --git a/lib/lp/answers/tests/test_questiontarget.py b/lib/lp/answers/tests/test_questiontarget.py
index c8f56e8..dbe73e7 100644
--- a/lib/lp/answers/tests/test_questiontarget.py
+++ b/lib/lp/answers/tests/test_questiontarget.py
@@ -3,8 +3,6 @@
 
 """Tests related to IQuestionTarget."""
 
-__all__ = []
-
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py
index 69ebb95..fa8a497 100644
--- a/lib/lp/app/__init__.py
+++ b/lib/lp/app/__init__.py
@@ -8,8 +8,6 @@ together. As such, it can import from any modules, but nothing should import
 from it.
 """
 
-__all__ = []
-
 # Zope recently changed the behaviour of items widgets with regards to missing
 # values, but they kindly left this global variable for you to monkey patch if
 # you want the old behaviour, just like we do.
diff --git a/lib/lp/app/browser/badge.py b/lib/lp/app/browser/badge.py
index d3fd5a6..cf4db60 100644
--- a/lib/lp/app/browser/badge.py
+++ b/lib/lp/app/browser/badge.py
@@ -11,12 +11,12 @@ Badges are shown in two main places:
 __all__ = [
     "Badge",
     "HasBadgeBase",
-    "IHasBadges",
     "STANDARD_BADGES",
 ]
 
-from zope.interface import Interface, implementer
+from zope.interface import implementer
 
+from lp.app.browser.interfaces import IHasBadges
 from lp.services.privacy.interfaces import IObjectPrivacy
 
 
@@ -109,20 +109,6 @@ STANDARD_BADGES = {
 }
 
 
-class IHasBadges(Interface):
-    """A method to determine visible badges.
-
-    Badges are used to show connections between different content objects, for
-    example a BugBranch is a link between a bug and a branch.  To represent
-    this link a bug has a branch badge, and the branch has a bug badge.
-
-    Badges should honour the visibility of the linked objects.
-    """
-
-    def getVisibleBadges():
-        """Return a list of `Badge` objects that the logged in user can see."""
-
-
 @implementer(IHasBadges)
 class HasBadgeBase:
     """The standard base implementation for badge visibility.
diff --git a/lib/lp/app/browser/interfaces.py b/lib/lp/app/browser/interfaces.py
new file mode 100644
index 0000000..32152d8
--- /dev/null
+++ b/lib/lp/app/browser/interfaces.py
@@ -0,0 +1,18 @@
+#  Copyright 2022 Canonical Ltd.  This software is licensed under the
+#  GNU Affero General Public License version 3 (see the file LICENSE).
+
+from zope.interface import Interface
+
+
+class IHasBadges(Interface):
+    """A method to determine visible badges.
+
+    Badges are used to show connections between different content objects, for
+    example a BugBranch is a link between a bug and a branch.  To represent
+    this link a bug has a branch badge, and the branch has a bug badge.
+
+    Badges should honour the visibility of the linked objects.
+    """
+
+    def getVisibleBadges():
+        """Return a list of `Badge` objects that the logged-in user can see."""
diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
index 201c948..01ed055 100644
--- a/lib/lp/app/browser/launchpadform.py
+++ b/lib/lp/app/browser/launchpadform.py
@@ -14,6 +14,8 @@ __all__ = [
     "safe_action",
 ]
 
+from typing import List, Optional, Type
+
 import simplejson
 import transaction
 from lazr.lifecycle.event import ObjectModifiedEvent
@@ -31,7 +33,7 @@ from zope.formlib.widgets import (
     RadioWidget,
     TextAreaWidget,
 )
-from zope.interface import classImplements, implementer, providedBy
+from zope.interface import Interface, classImplements, implementer, providedBy
 from zope.traversing.interfaces import ITraversable, TraversalError
 
 from lp.services.webapp.escaping import html_escape
@@ -60,15 +62,15 @@ class LaunchpadFormView(LaunchpadView):
     prefix = "field"
 
     # The form schema
-    schema = None
+    schema = None  # type: Type[Interface]
     # Subset of fields to use
-    field_names = None
+    field_names = None  # type: Optional[List[str]]
 
     # The next URL to redirect to on successful form submission
-    next_url = None
+    next_url = None  # type: Optional[str]
     # The cancel URL is rendered as a Cancel link in the form
     # macro if set in a derived class.
-    cancel_url = None
+    cancel_url = None  # type: Optional[str]
 
     # The name of the widget that will receive initial focus in the form.
     # By default, the first widget will receive focus.  Set this to None
@@ -87,7 +89,7 @@ class LaunchpadFormView(LaunchpadView):
     # The for_input is passed through to create the fields.  If this value
     # is set to true in derived classes, then fields that are marked
     # read only will have editable widgets created for them.
-    for_input = None
+    for_input = None  # type: Optional[bool]
 
     def __init__(self, context, request):
         LaunchpadView.__init__(self, context, request)
@@ -568,8 +570,13 @@ class ReturnToReferrerMixin:
         else:
             return canonical_url(self.context)
 
-    next_url = _return_url
-    cancel_url = _return_url
+    @property
+    def next_url(self):
+        return self._return_url
+
+    @property
+    def cancel_url(self):
+        return self._return_url
 
 
 def has_structured_doc(field):
diff --git a/lib/lp/app/browser/multistep.py b/lib/lp/app/browser/multistep.py
index a6b7aee..a8df433 100644
--- a/lib/lp/app/browser/multistep.py
+++ b/lib/lp/app/browser/multistep.py
@@ -8,6 +8,7 @@ __all__ = [
     "StepView",
 ]
 
+from typing import List
 
 from zope.formlib import form
 from zope.formlib.widget import CustomWidgetFactory
@@ -148,7 +149,7 @@ class StepView(LaunchpadFormView):
         TextWidget, visible=False
     )
 
-    _field_names = []
+    _field_names = []  # type: List[str]
     step_name = ""
     main_action_label = "Continue"
     next_step = None
diff --git a/lib/lp/app/browser/root.py b/lib/lp/app/browser/root.py
index 7d5acda..5dc4181 100644
--- a/lib/lp/app/browser/root.py
+++ b/lib/lp/app/browser/root.py
@@ -10,6 +10,7 @@ __all__ = [
 
 import re
 import time
+from typing import Any, List
 
 import feedparser
 import requests
@@ -55,7 +56,7 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
     """An view for the default view of the LaunchpadRoot."""
 
     page_title = "Launchpad"
-    featured_projects = []
+    featured_projects = []  # type: List[Any]
     featured_projects_top = None
 
     # Used by the footer to display the lp-arcana section.
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index fe62ec3..ff29b5f 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -32,7 +32,7 @@ from zope.traversing.interfaces import (
 )
 
 from lp import _
-from lp.app.browser.badge import IHasBadges
+from lp.app.browser.interfaces import IHasBadges
 from lp.app.browser.stringformatter import FormattersAPI
 from lp.app.enums import PRIVATE_INFORMATION_TYPES
 from lp.app.interfaces.launchpad import (
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 05e5fee..dff47ee 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -4,6 +4,7 @@
 """Test vocabulary adapters."""
 
 from datetime import datetime
+from typing import List
 from urllib.parse import urlencode
 
 import pytz
@@ -23,6 +24,7 @@ from lp.app.errors import UnexpectedFormData
 from lp.registry.interfaces.irc import IIrcIDSet
 from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.model.person import Person
 from lp.services.webapp.interfaces import ILaunchpadRoot
 from lp.services.webapp.vocabulary import (
     CountableIterator,
@@ -506,16 +508,16 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
 
 @implementer(IHugeVocabulary)
 class TestPersonVocabulary:
-    test_persons = []
+    test_persons = []  # type: List[Person]
 
     @classmethod
-    def setTestData(cls, person_list):
+    def setTestData(cls, person_list: List[Person]):
         cls.test_persons = person_list
 
     def __init__(self, context):
         self.context = context
 
-    def toTerm(self, person):
+    def toTerm(self, person: Person):
         return SimpleTerm(person, person.name, person.displayname)
 
     def searchForTerms(self, query=None, vocab_filter=None):
diff --git a/lib/lp/app/browser/tests/test_webservice.py b/lib/lp/app/browser/tests/test_webservice.py
index 2f3850d..b1f72ad 100644
--- a/lib/lp/app/browser/tests/test_webservice.py
+++ b/lib/lp/app/browser/tests/test_webservice.py
@@ -54,7 +54,8 @@ class BaseMissingObjectWebService:
     """Base test of NotFound errors for top-level webservice objects."""
 
     layer = DatabaseFunctionalLayer
-    object_type = None
+
+    object_type = None  # type: str
 
     def test_object_not_found(self):
         """Missing top-level objects generate 404s but not OOPS."""
diff --git a/lib/lp/app/browser/webservice.py b/lib/lp/app/browser/webservice.py
index 8439f7e..0d02cb0 100644
--- a/lib/lp/app/browser/webservice.py
+++ b/lib/lp/app/browser/webservice.py
@@ -3,8 +3,6 @@
 
 """Adapters for registry objects for the webservice."""
 
-__all__ = []
-
 from lazr.restful.interfaces import (
     IFieldHTMLRenderer,
     IReference,
diff --git a/lib/lp/app/doc/badges.rst b/lib/lp/app/doc/badges.rst
index 3e7dfcc..b2bf541 100644
--- a/lib/lp/app/doc/badges.rst
+++ b/lib/lp/app/doc/badges.rst
@@ -85,7 +85,8 @@ implementation of IHasBadges. HasBadgeBase is also a default adapter
 for Interface, which just provides the privacy badge.
 
     >>> from zope.interface import Interface, Attribute, implementer
-    >>> from lp.app.browser.badge import IHasBadges, HasBadgeBase
+    >>> from lp.app.browser.interfaces import IHasBadges
+    >>> from lp.app.browser.badge import HasBadgeBase
     >>> from lp.testing import verifyObject
     >>> @implementer(Interface)
     ... class PrivateClass:
@@ -196,7 +197,7 @@ IHasBadges.  Here is the sample from the branch.zcml to illustrate.
 
   <adapter
       for="lp.code.interfaces.branch.IBranch"
-      provides="lp.app.browser.badge.IHasBadges"
+      provides="lp.app.browser.interfaces.IHasBadges"
       factory="lp.code.browser.branchlisting.BranchBadges"
       />
 
diff --git a/lib/lp/app/security.py b/lib/lp/app/security.py
index 30ec73b..37ccc27 100644
--- a/lib/lp/app/security.py
+++ b/lib/lp/app/security.py
@@ -10,9 +10,10 @@ __all__ = [
 ]
 
 from itertools import repeat
+from typing import Optional, Type
 
 from zope.component import queryAdapter
-from zope.interface import implementer
+from zope.interface import Interface, implementer
 from zope.security.permission import checkPermission
 
 from lp.app.interfaces.security import IAuthorization
@@ -20,8 +21,8 @@ from lp.app.interfaces.security import IAuthorization
 
 @implementer(IAuthorization)
 class AuthorizationBase:
-    permission = None
-    usedfor = None
+    permission = None  # type: Optional[str]
+    usedfor = None  # type: Optional[Type[Interface]]
 
     def __init__(self, obj):
         self.obj = obj
diff --git a/lib/lp/app/tests/test_yuitests.py b/lib/lp/app/tests/test_yuitests.py
index 371a972..1b12917 100644
--- a/lib/lp/app/tests/test_yuitests.py
+++ b/lib/lp/app/tests/test_yuitests.py
@@ -3,8 +3,6 @@
 
 """Run YUI.test tests."""
 
-__all__ = []
-
 from lp.testing import YUIUnitTestCase, build_yui_unittest_suite
 from lp.testing.layers import YUITestLayer
 
diff --git a/lib/lp/app/utilities/celebrities.py b/lib/lp/app/utilities/celebrities.py
index 3df5989..3f54349 100644
--- a/lib/lp/app/utilities/celebrities.py
+++ b/lib/lp/app/utilities/celebrities.py
@@ -5,6 +5,8 @@
 
 __all__ = ["LaunchpadCelebrities"]
 
+from typing import Set
+
 from zope.component import getUtility
 from zope.interface import implementer
 
@@ -102,7 +104,8 @@ class PersonCelebrityDescriptor(CelebrityDescriptor):
     if a given person is a celebrity for special handling.
     """
 
-    names = set()  # Populated by the constructor.
+    # Populated by the constructor.
+    names = set()  # type: Set[str]
 
     def __init__(self, name):
         PersonCelebrityDescriptor.names.add(name)
diff --git a/lib/lp/app/validators/__init__.py b/lib/lp/app/validators/__init__.py
index 1806143..c0fbb40 100644
--- a/lib/lp/app/validators/__init__.py
+++ b/lib/lp/app/validators/__init__.py
@@ -12,20 +12,18 @@ See README.txt for discussion
 from zope.formlib.exception import (
     WidgetInputErrorView as Z3WidgetInputErrorView,
 )
-from zope.formlib.interfaces import IWidgetInputError
-from zope.interface import Interface, implementer
+from zope.interface import implementer
 from zope.schema.interfaces import ValidationError
 
+from lp.app.validators.interfaces import (
+    ILaunchpadValidationError,
+    ILaunchpadWidgetInputErrorView,
+)
 from lp.services.webapp.escaping import html_escape
 
 __all__ = ["LaunchpadValidationError"]
 
 
-class ILaunchpadValidationError(IWidgetInputError):
-    def snippet():
-        """Render as an HTML error message, as per IWidgetInputErrorView"""
-
-
 @implementer(ILaunchpadValidationError)
 class LaunchpadValidationError(ValidationError):
     """A LaunchpadValidationError may be raised from a schema field
@@ -69,15 +67,6 @@ class LaunchpadValidationError(ValidationError):
         return self.snippet()
 
 
-class ILaunchpadWidgetInputErrorView(Interface):
-    def snippet():
-        """Convert a widget input error to an html snippet
-
-        If the error implements provides a snippet() method, just return it.
-        Otherwise, fall back to the default Z3 mechanism
-        """
-
-
 @implementer(ILaunchpadWidgetInputErrorView)
 class WidgetInputErrorView(Z3WidgetInputErrorView):
     """Display an input error as a snippet of text.
diff --git a/lib/lp/app/validators/interfaces.py b/lib/lp/app/validators/interfaces.py
new file mode 100644
index 0000000..ff631bd
--- /dev/null
+++ b/lib/lp/app/validators/interfaces.py
@@ -0,0 +1,18 @@
+#  Copyright 2022 Canonical Ltd.  This software is licensed under the
+#  GNU Affero General Public License version 3 (see the file LICENSE).
+from zope.formlib.interfaces import IWidgetInputError
+from zope.interface import Interface
+
+
+class ILaunchpadValidationError(IWidgetInputError):
+    def snippet():
+        """Render as an HTML error message, as per IWidgetInputErrorView"""
+
+
+class ILaunchpadWidgetInputErrorView(Interface):
+    def snippet():
+        """Convert a widget input error to an html snippet
+
+        If the error implements provides a snippet() method, just return it.
+        Otherwise, fall back to the default Z3 mechanism
+        """
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 25b090d..66fb615 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -158,8 +158,7 @@ class DateTimeWidget(TextWidget):
 
         return [o.strip() for o in outputs]
 
-    # @property  XXX: do as a property when we have python2.5 for tests of
-    # properties
+    @property
     def time_zone(self):
         """The widget time zone.
 
@@ -215,8 +214,6 @@ class DateTimeWidget(TextWidget):
         ), "DateTime widget needs a time zone."
         return self.system_time_zone
 
-    time_zone = property(time_zone, doc=time_zone.__doc__)
-
     @property
     def time_zone_name(self):
         """The name of the widget time zone for display in the widget."""
@@ -251,8 +248,7 @@ class DateTimeWidget(TextWidget):
         else:
             return None
 
-    # @property  XXX: do as a property when we have python2.5 for tests of
-    # properties
+    @property
     def daterange(self):
         """The javascript variable giving the allowed date range to pick.
 
@@ -318,8 +314,6 @@ class DateTimeWidget(TextWidget):
             daterange += self.to_date.strftime("[%Y,%m,%d]]")
         return daterange
 
-    daterange = property(daterange, doc=daterange.__doc__)
-
     def getInputValue(self):
         """Return the date, if it is in the allowed date range."""
         value = super().getInputValue()
diff --git a/lib/lp/app/widgets/tests/test_itemswidgets.py b/lib/lp/app/widgets/tests/test_itemswidgets.py
index 8d17443..57bdae9 100644
--- a/lib/lp/app/widgets/tests/test_itemswidgets.py
+++ b/lib/lp/app/widgets/tests/test_itemswidgets.py
@@ -2,6 +2,7 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import doctest
+from typing import Any, Type
 
 from lazr.enum import EnumeratedType, Item
 from lazr.enum._enum import DBEnumeratedType, DBItem
@@ -31,7 +32,7 @@ class ItemWidgetTestCase(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
-    WIDGET_CLASS = None
+    WIDGET_CLASS = None  # type: Type[Any]
     SAFE_TERM = SimpleTerm("object-1", "token-1", "Safe title")
     UNSAFE_TERM = SimpleTerm("object-2", "token-2", "<unsafe> &nbsp; title")
 
diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
index e2fa1e8..aeb16c9 100644
--- a/lib/lp/archivepublisher/artifactory.py
+++ b/lib/lp/archivepublisher/artifactory.py
@@ -41,14 +41,16 @@ def _path_for(
     source_name: str,
     source_version: str,
     pub_file: IPackageReleaseFile,
-) -> Path:
+) -> ArtifactoryPath:
     repository_format = archive.repository_format
     if repository_format == ArchiveRepositoryFormat.DEBIAN:
         path = rootpath / poolify(source_name)
     elif repository_format == ArchiveRepositoryFormat.PYTHON:
         path = rootpath / source_name / source_version
     elif repository_format == ArchiveRepositoryFormat.CONDA:
-        user_defined_fields = pub_file.binarypackagerelease.user_defined_fields
+        user_defined_fields = (
+            pub_file.binarypackagerelease.user_defined_fields  # type: ignore
+        )
         subdir = next(
             (value for key, value in user_defined_fields if key == "subdir"),
             None,
@@ -86,7 +88,7 @@ class ArtifactoryPoolEntry:
     def debug(self, *args, **kwargs) -> None:
         self.logger.debug(*args, **kwargs)
 
-    def pathFor(self, component: Optional[str] = None) -> Path:
+    def pathFor(self, component: Optional[str] = None) -> ArtifactoryPath:
         """Return the path for this file in the given component."""
         # For Artifactory publication, we ignore the component.  There's
         # only marginal benefit in having it be explicitly represented in
@@ -113,9 +115,13 @@ class ArtifactoryPoolEntry:
         be set as the "launchpad.release-id" property to keep track of this.
         """
         if ISourcePackageReleaseFile.providedBy(pub_file):
-            return "source:%d" % pub_file.sourcepackagereleaseID
+            return "source:{:d}".format(
+                pub_file.sourcepackagereleaseID
+            )  # type: ignore
         elif IBinaryPackageFile.providedBy(pub_file):
-            return "binary:%d" % pub_file.binarypackagereleaseID
+            return "binary:{:d}".format(
+                pub_file.binarypackagereleaseID
+            )  # type: ignore
         else:
             raise AssertionError("Unsupported file: %r" % pub_file)
 
@@ -382,6 +388,7 @@ class ArtifactoryPool:
         # the pool structure, and doing so would introduce significant
         # complications in terms of having to keep track of components just
         # in order to update an artifact's properties.
+        assert pub_file is not None
         return _path_for(
             self.archive, self.rootpath, source_name, source_version, pub_file
         )
diff --git a/lib/lp/archivepublisher/customupload.py b/lib/lp/archivepublisher/customupload.py
index 6193ca4..533e488 100644
--- a/lib/lp/archivepublisher/customupload.py
+++ b/lib/lp/archivepublisher/customupload.py
@@ -113,7 +113,7 @@ class CustomUpload:
     """Base class for custom upload handlers"""
 
     # This should be set as a class property on each subclass.
-    custom_type = None
+    custom_type = None  # type: str
 
     @classmethod
     def publish(cls, packageupload, libraryfilealias, logger=None):
diff --git a/lib/lp/archivepublisher/debversion.py b/lib/lp/archivepublisher/debversion.py
index 7fd1ac5..e50c6fa 100644
--- a/lib/lp/archivepublisher/debversion.py
+++ b/lib/lp/archivepublisher/debversion.py
@@ -19,7 +19,9 @@ valid_epoch = re.compile(r"^[0-9]+$")
 valid_upstream = re.compile(r"^[0-9][A-Za-z0-9+:.~-]*$")
 valid_revision = re.compile(r"^[A-Za-z0-9+.~]+$")
 
-VersionError = changelog.VersionError
+
+class VersionError(changelog.VersionError):
+    pass
 
 
 class BadInputError(VersionError):
diff --git a/lib/lp/archivepublisher/diskpool.py b/lib/lp/archivepublisher/diskpool.py
index dfa3e3e..cac6989 100644
--- a/lib/lp/archivepublisher/diskpool.py
+++ b/lib/lp/archivepublisher/diskpool.py
@@ -131,7 +131,7 @@ class DiskPoolEntry:
     the disk for this file.
 
     'tempath' must be in the same filesystem as 'rootpath', it will be
-    used to store the instalation candidate while it is being downloaded
+    used to store the installation candidate while it is being downloaded
     from the Librarian.
 
     Remaining files in the 'temppath' indicated installation failures and
@@ -203,9 +203,12 @@ class DiskPoolEntry:
             if component in components:
                 return component
 
+        return None
+
     @cachedproperty
     def file_hash(self) -> str:
         """Return the SHA1 sum of this file."""
+        assert self.file_component is not None
         targetpath = self.pathFor(self.file_component)
         return sha1_from_path(str(targetpath))
 
@@ -308,6 +311,7 @@ class DiskPoolEntry:
             # shuffle the symlinks, so that the one we want to delete will
             # just be one of the links, and becomes safe.
             targetcomponent = self.preferredComponent(remove=component)
+            assert targetcomponent is not None
             self._shufflesymlinks(targetcomponent)
 
         return self._reallyRemove(component)
@@ -336,6 +340,8 @@ class DiskPoolEntry:
     def _shufflesymlinks(self, targetcomponent: str) -> None:
         """Shuffle the symlinks for filename so that targetcomponent contains
         the real file and the rest are symlinks to the right place..."""
+        assert self.file_component is not None
+
         if targetcomponent == self.file_component:
             # We're already in the right place.
             return
@@ -405,6 +411,7 @@ class DiskPoolEntry:
         """
         component = self.preferredComponent()
         if not self.file_component == component:
+            assert component is not None
             self._shufflesymlinks(component)
 
 
@@ -427,7 +434,6 @@ class DiskPool:
         self.archive = archive
         self.rootpath = Path(rootpath)
         self.temppath = Path(temppath) if temppath is not None else None
-        self.entries = {}
         self.logger = logger
 
     def _getEntry(
@@ -437,6 +443,7 @@ class DiskPool:
         pub_file: IPackageReleaseFile,
     ) -> DiskPoolEntry:
         """Return a new DiskPoolEntry for the given source and file."""
+        assert self.temppath is not None
         return DiskPoolEntry(
             self.archive,
             self.rootpath,
@@ -457,6 +464,7 @@ class DiskPool:
     ) -> Path:
         """Return the path for the given pool file."""
         if file is None:
+            assert pub_file is not None
             file = pub_file.libraryfile.filename
         if file is None:
             raise AssertionError("Must pass either pub_file or file")
diff --git a/lib/lp/archiveuploader/dscfile.py b/lib/lp/archiveuploader/dscfile.py
index c72c28f..cc86e4e 100644
--- a/lib/lp/archiveuploader/dscfile.py
+++ b/lib/lp/archiveuploader/dscfile.py
@@ -394,14 +394,13 @@ class DSCFile(SourceUploadFile, SignableTagFile):
         all exceptions that are generated while processing DSC file checks.
         """
 
-        for error in SourceUploadFile.verify(self):
-            yield error
+        yield from SourceUploadFile.verify(self)
 
         # Check size and checksum of the DSC file itself
         try:
             self.checkSizeAndCheckSum()
-        except UploadError as error:
-            yield error
+        except UploadError as e:
+            yield e
 
         try:
             raw_files = parse_and_merge_file_lists(self._dict, changes=False)
@@ -426,8 +425,8 @@ class DSCFile(SourceUploadFile, SignableTagFile):
                 file_instance = DSCUploadedFile(
                     filepath, hashes, size, self.policy, self.logger
                 )
-            except UploadError as error:
-                yield error
+            except UploadError as e:
+                yield e
             else:
                 files.append(file_instance)
         self.files = files
@@ -463,10 +462,10 @@ class DSCFile(SourceUploadFile, SignableTagFile):
                     with warnings.catch_warnings():
                         warnings.simplefilter("error")
                         PkgRelation.parse_relations(field)
-                except Warning as error:
+                except Warning as e:
                     yield UploadError(
                         "%s: invalid %s field; cannot be parsed by deb822: %s"
-                        % (self.filename, field_name, error)
+                        % (self.filename, field_name, e)
                     )
 
         # Verify if version declared in changesfile is the same than that
@@ -478,8 +477,7 @@ class DSCFile(SourceUploadFile, SignableTagFile):
                 % (self.filename, self.dsc_version, self.version)
             )
 
-        for error in self.checkFiles():
-            yield error
+        yield from self.checkFiles()
 
     def _getFileByName(self, filename):
         """Return the corresponding file reference in the policy context.
diff --git a/lib/lp/archiveuploader/tests/test_buildduploads.py b/lib/lp/archiveuploader/tests/test_buildduploads.py
index 3070282..56a20fc 100644
--- a/lib/lp/archiveuploader/tests/test_buildduploads.py
+++ b/lib/lp/archiveuploader/tests/test_buildduploads.py
@@ -25,9 +25,9 @@ from lp.testing.gpgkeys import import_public_test_keys
 class TestStagedBinaryUploadBase(TestUploadProcessorBase):
     name = "baz"
     version = "1.0-1"
-    distribution_name = None
-    distroseries_name = None
-    pocket = None
+    distribution_name = None  # type: str
+    distroseries_name = None  # type: str
+    pocket = None  # type: PackagePublishingPocket
     policy = "buildd"
     no_mails = True
 
diff --git a/lib/lp/blueprints/browser/specification.py b/lib/lp/blueprints/browser/specification.py
index 82f9fb5..36c4a8e 100644
--- a/lib/lp/blueprints/browser/specification.py
+++ b/lib/lp/blueprints/browser/specification.py
@@ -39,6 +39,7 @@ __all__ = [
 import os
 from operator import attrgetter
 from subprocess import PIPE, Popen
+from typing import List
 
 import six
 from lazr.restful.interface import copy_field, use_template
@@ -516,7 +517,7 @@ class SpecificationActionMenu(NavigationMenu, SpecificationEditLinksMixin):
 
     usedfor = ISpecification
     facet = "specifications"
-    links = ("edit", "supersede", "retarget")
+    links = ["edit", "supersede", "retarget"]
 
 
 class SpecificationContextMenu(ContextMenu, SpecificationEditLinksMixin):
@@ -965,7 +966,9 @@ class SpecificationInformationTypeEditView(LaunchpadFormView):
         """Return the next URL to call when this call completes."""
         return None
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @property
     def initial_values(self):
@@ -1043,7 +1046,7 @@ class SpecificationGoalDecideView(LaunchpadFormView):
     """
 
     schema = Interface
-    field_names = []
+    field_names = []  # type: List[str]
 
     @property
     def label(self):
@@ -1061,7 +1064,9 @@ class SpecificationGoalDecideView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class ISpecificationRetargetingSchema(Interface):
@@ -1645,7 +1650,9 @@ class SpecificationLinkBranchView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class SpecificationSetView(AppFrontPageSearchView, HasSpecificationsView):
diff --git a/lib/lp/blueprints/browser/specificationbranch.py b/lib/lp/blueprints/browser/specificationbranch.py
index 5b54cba..1ee6fe9 100644
--- a/lib/lp/blueprints/browser/specificationbranch.py
+++ b/lib/lp/blueprints/browser/specificationbranch.py
@@ -9,6 +9,8 @@ __all__ = [
     "SpecificationBranchURL",
 ]
 
+from typing import List
+
 from zope.interface import implementer
 
 from lp import _
@@ -45,7 +47,7 @@ class SpecificationBranchStatusView(LaunchpadEditFormView):
     """Edit the summary of the SpecificationBranch link."""
 
     schema = ISpecificationBranch
-    field_names = []
+    field_names = []  # type: List[str]
     label = _("Delete link between specification and branch")
 
     def initialize(self):
@@ -84,7 +86,9 @@ class BranchLinkToSpecificationView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action(_("Continue"), name="continue")
     def continue_action(self, action, data):
diff --git a/lib/lp/blueprints/browser/specificationsubscription.py b/lib/lp/blueprints/browser/specificationsubscription.py
index 79598e8..bb93933 100644
--- a/lib/lp/blueprints/browser/specificationsubscription.py
+++ b/lib/lp/blueprints/browser/specificationsubscription.py
@@ -9,6 +9,8 @@ __all__ = [
     "SpecificationSubscriptionEditView",
 ]
 
+from typing import List
+
 from lazr.delegates import delegate_to
 from simplejson import dumps
 from zope.component import getUtility
@@ -37,10 +39,12 @@ class SpecificationSubscriptionAddView(LaunchpadFormView):
     label = "Subscribe to blueprint"
 
     @property
-    def cancel_url(self):
+    def next_url(self):
         return canonical_url(self.context)
 
-    next_url = cancel_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     def _subscribe(self, person, essential):
         self.context.subscribe(person, self.user, essential)
@@ -75,7 +79,7 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
     """Used to unsubscribe someone from a blueprint."""
 
     schema = ISpecificationSubscription
-    field_names = []
+    field_names = []  # type: List[str]
 
     @property
     def label(self):
@@ -87,10 +91,12 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
     page_title = label
 
     @property
-    def cancel_url(self):
+    def next_url(self):
         return canonical_url(self.context.specification)
 
-    next_url = cancel_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action("Unsubscribe", name="unsubscribe")
     def unsubscribe_action(self, action, data):
@@ -116,10 +122,12 @@ class SpecificationSubscriptionEditView(LaunchpadEditFormView):
         return "Modify subscription to %s" % self.context.specification.title
 
     @property
-    def cancel_url(self):
+    def next_url(self):
         return canonical_url(self.context.specification)
 
-    next_url = cancel_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action(_("Change"), name="change")
     def change_action(self, action, data):
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index 4a2be23..047084a 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -25,6 +25,7 @@ __all__ = [
 import csv
 import io
 from collections import defaultdict
+from typing import List
 
 import pytz
 from lazr.restful.utils import smartquote
@@ -401,7 +402,7 @@ class SprintDeleteView(LaunchpadFormView):
     """Form for deleting sprints."""
 
     schema = ISprint
-    field_names = []
+    field_names = []  # type: List[str]
 
     @property
     def label(self):
@@ -430,7 +431,9 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
             'Review discussion topics for "%s" sprint' % self.context.title
         )
 
-    page_title = label
+    @property
+    def page_title(self):
+        return self.label
 
     def initialize(self):
         self.status_message = None
@@ -591,13 +594,13 @@ class SprintSetNavigationMenu(RegistryCollectionActionMenuBase):
     """Action menu for sprints index."""
 
     usedfor = ISprintSet
-    links = (
+    links = [
         "register_team",
         "register_project",
         "register_sprint",
         "create_account",
         "view_all_sprints",
-    )
+    ]
 
     @enabled_with_permission("launchpad.View")
     def register_sprint(self):
diff --git a/lib/lp/blueprints/browser/sprintattendance.py b/lib/lp/blueprints/browser/sprintattendance.py
index acfa792..2964ffd 100644
--- a/lib/lp/blueprints/browser/sprintattendance.py
+++ b/lib/lp/blueprints/browser/sprintattendance.py
@@ -133,7 +133,9 @@ class BaseSprintAttendanceAddView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     _local_timeformat = "%H:%M on %A, %Y-%m-%d"
 
diff --git a/lib/lp/blueprints/mail/__init__.py b/lib/lp/blueprints/mail/__init__.py
index 6b3f0d4..516532e 100644
--- a/lib/lp/blueprints/mail/__init__.py
+++ b/lib/lp/blueprints/mail/__init__.py
@@ -1,4 +1,2 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
-
-__all__ = []
diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
index cbc391c..5f0a235 100644
--- a/lib/lp/bugs/browser/bug.py
+++ b/lib/lp/bugs/browser/bug.py
@@ -27,6 +27,7 @@ __all__ = [
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 import re
+from typing import Type
 
 from lazr.enum import (
     EnumeratedType,
@@ -216,7 +217,7 @@ class BugSetNavigation(Navigation):
 
 class BugContextMenu(ContextMenu):
     """Context menu of actions that can be performed upon a Bug."""
-    usedfor = IBug
+    usedfor = IBug  # type: Type[Interface]
     links = [
         'editdescription', 'markduplicate', 'visibility', 'addupstream',
         'adddistro', 'subscription', 'addsubscriber', 'editsubscriptions',
@@ -400,11 +401,13 @@ class MaloneView(LaunchpadFormView):
     schema = IFrontPageBugTaskSearch
     field_names = ['searchtext', 'scope']
 
-    # Test: standalone/xx-slash-malone-slash-bugs.rst
-    error_message = None
-
     page_title = 'Launchpad Bugs'
 
+    # Test: standalone/xx-slash-malone-slash-bugs.rst
+    @property
+    def error_message(self):
+        return None
+
     @property
     def target_css_class(self):
         """The CSS class for used in the target widget."""
@@ -753,7 +756,9 @@ class BugEditViewBase(LaunchpadEditFormView):
         """Return the next URL to call when this call completes."""
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class BugEditView(BugEditViewBase):
@@ -768,7 +773,9 @@ class BugEditView(BugEditViewBase):
         """The form label."""
         return 'Edit details for bug #%d' % self.context.bug.id
 
-    page_title = label
+    @property
+    def page_title(self):
+        return self.label
 
     @action('Change', name='change')
     def change_action(self, action, data):
@@ -840,7 +847,9 @@ class BugLockStatusEditView(LaunchpadEditFormView):
             return canonical_url(self.context)
         return None
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class BugMarkAsDuplicateView(BugEditViewBase):
@@ -954,7 +963,9 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
             return canonical_url(self.context)
         return None
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @property
     def initial_values(self):
diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py
index 7372c56..375af7f 100644
--- a/lib/lp/bugs/browser/bugalsoaffects.py
+++ b/lib/lp/bugs/browser/bugalsoaffects.py
@@ -8,6 +8,10 @@ __all__ = [
     ]
 
 from textwrap import dedent
+from typing import (
+    Tuple,
+    Type,
+    )
 
 from lazr.enum import (
     EnumeratedType,
@@ -227,7 +231,7 @@ class BugTaskCreationStep(AlsoAffectsStep):
 
     initial_focus_widget = 'bug_url'
     step_name = 'specify_remote_bug_url'
-    target_field_names = ()
+    target_field_names = ()  # type: Tuple[str, ...]
 
     # This is necessary so that other views which dispatch work to this one
     # have access to the newly created task.
@@ -710,7 +714,7 @@ class BugTrackerCreationStep(AlsoAffectsStep):
         StrippedTextWidget, displayWidth=62)
     step_name = "bugtracker_creation"
     main_action_label = 'Register Bug Tracker and Add to Bug Report'
-    _next_step = None
+    _next_step = None  # type: Type[StepView]
 
     def main_action(self, data):
         assert self._next_step is not None, (
diff --git a/lib/lp/bugs/browser/bugbranch.py b/lib/lp/bugs/browser/bugbranch.py
index e7ea7ab..eb67251 100644
--- a/lib/lp/bugs/browser/bugbranch.py
+++ b/lib/lp/bugs/browser/bugbranch.py
@@ -10,6 +10,8 @@ __all__ = [
     'BugBranchView',
     ]
 
+from typing import List
+
 from lazr.restful.interfaces import IWebServiceClientRequest
 from zope.component import (
     adapter,
@@ -65,14 +67,16 @@ class BugBranchAddView(LaunchpadFormView):
     def label(self):
         return 'Add a branch to bug #%i' % self.context.bug.id
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class BugBranchDeleteView(LaunchpadEditFormView):
     """View to update a BugBranch."""
     schema = IBugBranch
 
-    field_names = []
+    field_names = []  # type: List[str]
 
     def initialize(self):
         LaunchpadEditFormView.initialize(self)
@@ -81,7 +85,9 @@ class BugBranchDeleteView(LaunchpadEditFormView):
     def next_url(self):
         return canonical_url(self.context.bug)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action('Remove link', name='delete')
     def delete_action(self, action, data):
@@ -129,7 +135,9 @@ class BranchLinkToBugView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action(_('Continue'), name='continue')
     def continue_action(self, action, data):
diff --git a/lib/lp/bugs/browser/bugnomination.py b/lib/lp/bugs/browser/bugnomination.py
index 40dddbd..d6eec68 100644
--- a/lib/lp/bugs/browser/bugnomination.py
+++ b/lib/lp/bugs/browser/bugnomination.py
@@ -10,6 +10,7 @@ __all__ = [
     'BugNominationTableRowView']
 
 import datetime
+from typing import List
 
 import pytz
 from zope.component import getUtility
@@ -204,7 +205,7 @@ class BugNominationEditView(LaunchpadFormView):
     """Browser view class for approving and declining nominations."""
 
     schema = Interface
-    field_names = []
+    field_names = []  # type: List[str]
 
     @property
     def label(self):
diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py
index b28876c..401c30a 100644
--- a/lib/lp/bugs/browser/bugsubscription.py
+++ b/lib/lp/bugs/browser/bugsubscription.py
@@ -11,6 +11,8 @@ __all__ = [
     'BugSubscriptionListView',
     ]
 
+from typing import List
+
 from lazr.delegates import delegate_to
 from lazr.restful.interfaces import (
     IJSONRequestCache,
@@ -89,7 +91,9 @@ class BugSubscriptionAddView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @property
     def label(self):
@@ -192,7 +196,9 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
             next_url = context_url
         return next_url
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @cachedproperty
     def _subscribers_for_current_user(self):
@@ -654,7 +660,7 @@ class BugMuteSelfView(LaunchpadFormView):
     """A view to mute a user's bug mail for a given bug."""
 
     schema = IBugSubscription
-    field_names = []
+    field_names = []  # type: List[str]
 
     @property
     def label(self):
@@ -669,7 +675,9 @@ class BugMuteSelfView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     def initialize(self):
         self.is_muted = self.context.bug.isMuted(self.user)
diff --git a/lib/lp/bugs/browser/bugsubscriptionfilter.py b/lib/lp/bugs/browser/bugsubscriptionfilter.py
index e12219f..ff66f93 100644
--- a/lib/lp/bugs/browser/bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/bugsubscriptionfilter.py
@@ -106,14 +106,14 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
     """Base class for edit or create views of `IBugSubscriptionFilter`."""
 
     schema = IBugSubscriptionFilter
-    field_names = (
+    field_names = [
         "description",
         "statuses",
         "importances",
         "information_types",
         "tags",
         "find_all_tags",
-        )
+    ]
 
     custom_widget_description = CustomWidgetFactory(
         TextWidget, displayWidth=50)
@@ -148,7 +148,9 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
         return canonical_url(
             self.user, view_name="+structural-subscriptions")
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class BugSubscriptionFilterEditView(
diff --git a/lib/lp/bugs/browser/bugsupervisor.py b/lib/lp/bugs/browser/bugsupervisor.py
index 44040f2..80367de 100644
--- a/lib/lp/bugs/browser/bugsupervisor.py
+++ b/lib/lp/bugs/browser/bugsupervisor.py
@@ -55,7 +55,9 @@ class BugSupervisorEditView(LaunchpadEditFormView):
         """See `LaunchpadFormView`."""
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @action('Change', name='change')
     def change_action(self, action, data):
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 7228fc7..bc11e3e 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -33,6 +33,7 @@ from datetime import (
 from itertools import groupby
 from operator import attrgetter
 import re
+from typing import List
 from urllib.parse import quote
 
 from lazr.delegates import delegate_to
@@ -1599,7 +1600,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
     """Used to delete a bugtask."""
 
     schema = IBugTask
-    field_names = []
+    field_names = []  # type: List[str]
 
     label = 'Remove bug task'
     page_title = label
@@ -1611,6 +1612,13 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
             return self._next_url or self._return_url
         return None
 
+    @property
+    def cancel_url(self):
+        # We have to explicitly define `cancel_url` as a property here
+        # to make `mypy` happy - the base classes both define `cancel_url`
+        # in a non-compatible fashion
+        return super().cancel_url
+
     @action('Delete', name='delete_bugtask')
     def delete_bugtask_action(self, action, data):
         bugtask = self.context
diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py
index 49cfb21..a0832e9 100644
--- a/lib/lp/bugs/browser/bugtracker.py
+++ b/lib/lp/bugs/browser/bugtracker.py
@@ -488,7 +488,9 @@ class BugTrackerEditComponentView(LaunchpadEditFormView):
     def next_url(self):
         return canonical_url(self.context.component_group.bug_tracker)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     def updateContextFromData(self, data, context=None):
         """Link component to specified distro source package.
diff --git a/lib/lp/bugs/browser/bugwatch.py b/lib/lp/bugs/browser/bugwatch.py
index 516adad..8ee1274 100644
--- a/lib/lp/bugs/browser/bugwatch.py
+++ b/lib/lp/bugs/browser/bugwatch.py
@@ -166,7 +166,9 @@ class BugWatchEditView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(getUtility(ILaunchBag).bug)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class BugWatchActivityPortletView(LaunchpadFormView):
@@ -194,7 +196,9 @@ class BugWatchActivityPortletView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(getUtility(ILaunchBag).bug)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
     @property
     def recent_watch_activity(self):
diff --git a/lib/lp/bugs/browser/cve.py b/lib/lp/bugs/browser/cve.py
index 8b6ae28..372753d 100644
--- a/lib/lp/bugs/browser/cve.py
+++ b/lib/lp/bugs/browser/cve.py
@@ -109,7 +109,9 @@ class CveLinkView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
-    cancel_url = next_url
+    @property
+    def cancel_url(self):
+        return self.next_url
 
 
 class CveUnlinkView(CveLinkView):
@@ -124,9 +126,11 @@ class CveUnlinkView(CveLinkView):
 
     @property
     def label(self):
-        return  'Bug # %s Remove link to CVE report' % self.context.bug.id
+        return 'Bug # %s Remove link to CVE report' % self.context.bug.id
 
-    page_title = label
+    @property
+    def page_title(self):
+        return self.label
 
     heading = 'Remove links to bug reports'
 
diff --git a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
index 45ec289..2981b4d 100644
--- a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
@@ -36,8 +36,8 @@ from lp.testing.views import create_initialized_view
 
 class TestBugSubscriptionFilterBase:
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         self.owner = self.factory.makePerson(name="foo")
         self.structure = self.factory.makeProduct(
             owner=self.owner, name="bar")
diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
index 00151ed..7daefd9 100644
--- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
+++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
@@ -302,8 +302,8 @@ class FileBugViewMixin:
             # Disable redirects on validation failure.
             pass
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         self.target = self.factory.makeProduct()
         transaction.commit()
         login_person(self.target.owner)
diff --git a/lib/lp/bugs/externalbugtracker/base.py b/lib/lp/bugs/externalbugtracker/base.py
index 8cf7f0e..d1f08e9 100644
--- a/lib/lp/bugs/externalbugtracker/base.py
+++ b/lib/lp/bugs/externalbugtracker/base.py
@@ -25,7 +25,7 @@ __all__ = [
     'UnsupportedBugTrackerVersion',
     ]
 
-
+from typing import Optional
 from urllib.parse import (
     urljoin,
     urlparse,
@@ -159,7 +159,7 @@ def repost_on_redirect_hook(response, *args, **kwargs):
 class ExternalBugTracker:
     """Base class for an external bug tracker."""
 
-    batch_size = None
+    batch_size = None  # type: Optional[int]
     batch_query_threshold = config.checkwatches.batch_query_threshold
     timeout = config.checkwatches.default_socket_timeout
     comment_template = 'default_remotecomment_template.txt'
diff --git a/lib/lp/bugs/externalbugtracker/github.py b/lib/lp/bugs/externalbugtracker/github.py
index 8f45e4d..b3e60b2 100644
--- a/lib/lp/bugs/externalbugtracker/github.py
+++ b/lib/lp/bugs/externalbugtracker/github.py
@@ -7,8 +7,7 @@ __all__ = [
     'BadGitHubURL',
     'GitHub',
     'GitHubRateLimit',
-    'IGitHubRateLimit',
-    ]
+]
 
 from contextlib import contextmanager
 import http.client
@@ -21,7 +20,6 @@ from urllib.parse import (
 import pytz
 import requests
 from zope.component import getUtility
-from zope.interface import Interface
 
 from lp.bugs.externalbugtracker import (
     BugTrackerConnectError,
@@ -31,6 +29,7 @@ from lp.bugs.externalbugtracker import (
     UnparsableBugTrackerVersion,
     )
 from lp.bugs.externalbugtracker.base import LP_USER_AGENT
+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
 from lp.bugs.interfaces.bugtask import (
     BugTaskImportance,
     BugTaskStatus,
@@ -56,24 +55,6 @@ class GitHubExceededRateLimit(BugWatchUpdateError):
             self.host, time.ctime(self.reset))
 
 
-class IGitHubRateLimit(Interface):
-    """Interface for rate-limit tracking for the GitHub Issues API."""
-
-    def checkLimit(url, token=None):
-        """A context manager that checks the remote host's rate limit.
-
-        :param url: The URL being requested.
-        :param token: If not None, an OAuth token to use as authentication
-            to the remote host when asking it for the current rate limit.
-        :return: A suitable `Authorization` header (from the context
-            manager's `__enter__` method).
-        :raises GitHubExceededRateLimit: if the rate limit was exceeded.
-        """
-
-    def clearCache():
-        """Forget any cached rate limits."""
-
-
 class GitHubRateLimit:
     """Rate-limit tracking for the GitHub Issues API."""
 
diff --git a/lib/lp/bugs/externalbugtracker/interfaces.py b/lib/lp/bugs/externalbugtracker/interfaces.py
new file mode 100644
index 0000000..0ce8030
--- /dev/null
+++ b/lib/lp/bugs/externalbugtracker/interfaces.py
@@ -0,0 +1,22 @@
+#  Copyright 2022 Canonical Ltd.  This software is licensed under the
+#  GNU Affero General Public License version 3 (see the file LICENSE).
+
+from zope.interface import Interface
+
+
+class IGitHubRateLimit(Interface):
+    """Interface for rate-limit tracking for the GitHub Issues API."""
+
+    def checkLimit(url, token=None):
+        """A context manager that checks the remote host's rate limit.
+
+        :param url: The URL being requested.
+        :param token: If not None, an OAuth token to use as authentication
+            to the remote host when asking it for the current rate limit.
+        :return: A suitable `Authorization` header (from the context
+            manager's `__enter__` method).
+        :raises GitHubExceededRateLimit: if the rate limit was exceeded.
+        """
+
+    def clearCache():
+        """Forget any cached rate limits."""
diff --git a/lib/lp/bugs/externalbugtracker/tests/test_github.py b/lib/lp/bugs/externalbugtracker/tests/test_github.py
index b7644b5..85b4cc7 100644
--- a/lib/lp/bugs/externalbugtracker/tests/test_github.py
+++ b/lib/lp/bugs/externalbugtracker/tests/test_github.py
@@ -34,8 +34,8 @@ from lp.bugs.externalbugtracker.github import (
     BadGitHubURL,
     GitHub,
     GitHubExceededRateLimit,
-    IGitHubRateLimit,
     )
+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
 from lp.bugs.interfaces.bugtask import BugTaskStatus
 from lp.bugs.interfaces.bugtracker import BugTrackerType
 from lp.bugs.interfaces.externalbugtracker import IExternalBugTracker
diff --git a/lib/lp/bugs/interfaces/bugnotification.py b/lib/lp/bugs/interfaces/bugnotification.py
index 7d6f23a..1fffc4e 100644
--- a/lib/lp/bugs/interfaces/bugnotification.py
+++ b/lib/lp/bugs/interfaces/bugnotification.py
@@ -73,10 +73,10 @@ class IBugNotificationSet(Interface):
     def getDeferredNotifications():
         """Returns the deferred notifications.
 
-        A deferred noticiation is one that is pending but has no recipients.
+        A deferred notification is one that is pending but has no recipients.
         """
 
-    def addNotification(self, bug, is_comment, message, recipients, activity):
+    def addNotification(bug, is_comment, message, recipients, activity):
         """Create a new `BugNotification`.
 
         Create a new `BugNotification` object and the corresponding
diff --git a/lib/lp/bugs/model/bugtarget.py b/lib/lp/bugs/model/bugtarget.py
index 0f7f748..41fb0f9 100644
--- a/lib/lp/bugs/model/bugtarget.py
+++ b/lib/lp/bugs/model/bugtarget.py
@@ -206,6 +206,7 @@ class OfficialBugTag(Storm):
     product_id = Int(name='product')
     product = Reference(product_id, 'Product.id')
 
+    @property
     def target(self):
         """See `IOfficialBugTag`."""
         # A database constraint ensures that either distribution or
@@ -215,7 +216,8 @@ class OfficialBugTag(Storm):
         else:
             return self.product
 
-    def _settarget(self, target):
+    @target.setter
+    def target(self, target):
         """See `IOfficialBugTag`."""
         if IDistribution.providedBy(target):
             self.distribution = target
@@ -225,5 +227,3 @@ class OfficialBugTag(Storm):
             raise ValueError(
                 'The target of an OfficialBugTag must be either an '
                 'IDistribution instance or an IProduct instance.')
-
-    target = property(target, _settarget, doc=target.__doc__)
diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
index ce372d6..f96cc46 100644
--- a/lib/lp/bugs/model/tests/test_bugtask.py
+++ b/lib/lp/bugs/model/tests/test_bugtask.py
@@ -1093,7 +1093,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
+    def setUp(self, *args, **kwargs):
         """Create the test setup.
 
         We need
@@ -1104,7 +1104,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
           owners, bug supervisors, drivers
         - bug tasks for the targets
         """
-        super().setUp()
+        super().setUp(*args, **kwargs)
         self.target_owner_member = self.factory.makePerson()
         self.target_owner_team = self.factory.makeTeam(
             owner=self.target_owner_member,
diff --git a/lib/lp/bugs/model/tests/test_bugtask_status.py b/lib/lp/bugs/model/tests/test_bugtask_status.py
index 781a594..f3409e0 100644
--- a/lib/lp/bugs/model/tests/test_bugtask_status.py
+++ b/lib/lp/bugs/model/tests/test_bugtask_status.py
@@ -260,8 +260,8 @@ class TestBugTaskStatusTransitionForPrivilegedUserBase:
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         # Creation of task and target are deferred to subclasses.
         self.task = None
         self.person = None
@@ -281,7 +281,9 @@ class TestBugTaskStatusTransitionForPrivilegedUserBase:
         with person_logged_in(self.person):
             self.task.transitionToStatus(BugTaskStatus.WONTFIX, self.person)
             self.assertEqual(self.task.status, BugTaskStatus.WONTFIX)
-            self.task.transitionToStatus(BugTaskStatus.DOESNOTEXIST, self.person)
+            self.task.transitionToStatus(
+                BugTaskStatus.DOESNOTEXIST, self.person
+            )
             self.assertEqual(self.task.status, BugTaskStatus.DOESNOTEXIST)
             self.task.transitionToStatus(BugTaskStatus.EXPIRED, self.person)
             self.assertEqual(self.task.status, BugTaskStatus.EXPIRED)
diff --git a/lib/lp/bugs/scripts/checkwatches/base.py b/lib/lp/bugs/scripts/checkwatches/base.py
index 0a0cfce..002daad 100644
--- a/lib/lp/bugs/scripts/checkwatches/base.py
+++ b/lib/lp/bugs/scripts/checkwatches/base.py
@@ -131,7 +131,7 @@ class WorkingBase:
         self._transaction_manager = parent._transaction_manager
         self.logger = parent.logger
 
-    @property
+    @property  # type: ignore
     @contextmanager
     def interaction(self):
         """Context manager for interaction as the given user.
@@ -149,7 +149,7 @@ class WorkingBase:
         else:
             yield
 
-    @property
+    @property  # type: ignore
     @contextmanager
     def transaction(self):
         """Context manager to ring-fence database activity.
@@ -186,7 +186,7 @@ class WorkingBase:
             self._statement_logging_stop()
             self._statement_logging_start()
 
-    @property
+    @property  # type: ignore
     @contextmanager
     def statement_logging(self):
         """Context manager to start and stop SQL statement logging.
diff --git a/lib/lp/bugs/scripts/checkwatches/core.py b/lib/lp/bugs/scripts/checkwatches/core.py
index 069fb19..f95ae89 100644
--- a/lib/lp/bugs/scripts/checkwatches/core.py
+++ b/lib/lp/bugs/scripts/checkwatches/core.py
@@ -27,6 +27,7 @@ import socket
 import sys
 import threading
 import time
+from typing import List
 from xmlrpc.client import ProtocolError
 
 import pytz
@@ -70,7 +71,7 @@ from lp.services.scripts.logger import log as default_log
 LOGIN = 'bugwatch@xxxxxxxxxxxxxxxxxx'
 
 # A list of product names for which comments should be synchronized.
-SYNCABLE_GNOME_PRODUCTS = []
+SYNCABLE_GNOME_PRODUCTS = []  # type: List[str]
 
 # When syncing with a remote bug tracker that reports its idea of the
 # current time, this defined the maximum acceptable skew between the
diff --git a/lib/lp/bugs/scripts/debbugs.py b/lib/lp/bugs/scripts/debbugs.py
index 260b319..8c49d78 100644
--- a/lib/lp/bugs/scripts/debbugs.py
+++ b/lib/lp/bugs/scripts/debbugs.py
@@ -266,8 +266,9 @@ class Database:
             raise KeyError(bug_id)
         return bug
 
+
 if __name__ == '__main__':
-    for bug in Database('/srv/debzilla.no-name-yet.com/debbugs'):
+    for bug in Database('/srv/debzilla.no-name-yet.com/debbugs', None):
         try:
             print(bug, bug.subject)
         except Exception as e:
diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
index 2ea216f..9434e32 100644
--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
@@ -8,6 +8,12 @@ from datetime import (
     )
 import re
 from smtplib import SMTPException
+from typing import (
+    Any,
+    List,
+    Optional,
+    Type,
+    )
 import unittest
 
 from fixtures import FakeLogger
@@ -79,6 +85,7 @@ from lp.services.mail.helpers import (
 from lp.services.mail.sendmail import set_immediate_mail_delivery
 from lp.services.mail.stub import TestMailer
 from lp.services.messages.interfaces.message import IMessageSet
+from lp.services.messages.model.message import Message
 from lp.services.propertycache import cachedproperty
 from lp.testing import (
     login,
@@ -101,7 +108,7 @@ class MockBug:
 
     duplicateof = None
     information_type = InformationType.PUBLIC
-    messages = []
+    messages = []  # type: List[Message]
 
     def __init__(self, id, owner):
         self.id = id
@@ -686,7 +693,12 @@ class EmailNotificationTestBase(TestCaseWithFactory):
 
 class EmailNotificationsBugMixin:
 
-    change_class = change_name = old = new = alt = unexpected_bytes = None
+    change_class = None  # type: Optional[Type[Any]]
+    change_name = None  # type: Optional[str]
+    old = None  # type: Any
+    new = None  # type: Any
+    alt = None  # type: Any
+    unexpected_bytes = None  # type: Optional[bytes]
 
     def change(self, old, new):
         self.bug.addChange(
@@ -764,7 +776,7 @@ class EmailNotificationsBugTaskMixin(EmailNotificationsBugMixin):
 
 class EmailNotificationsAddedRemovedMixin:
 
-    old = new = added_message = removed_message = None
+    old = new = added_message = removed_message = b""
 
     def add(self, item):
         raise NotImplementedError
diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py
index ce1829a..920762b 100644
--- a/lib/lp/bugs/security.py
+++ b/lib/lp/bugs/security.py
@@ -3,8 +3,6 @@
 
 """Security adapters for the bugs module."""
 
-__all__ = []
-
 from lp.app.security import (
     AnonymousAuthorization,
     AuthorizationBase,
diff --git a/lib/lp/bugs/tests/externalbugtracker.py b/lib/lp/bugs/tests/externalbugtracker.py
index 11dcaab..29d038a 100644
--- a/lib/lp/bugs/tests/externalbugtracker.py
+++ b/lib/lp/bugs/tests/externalbugtracker.py
@@ -14,6 +14,11 @@ import os
 import random
 import re
 import time
+from typing import (
+    Any,
+    Dict,
+    Tuple,
+    )
 from urllib.parse import (
     parse_qs,
     urljoin,
@@ -522,7 +527,7 @@ class TestBugzillaXMLRPCTransport(RequestsTransport):
         'add_comment',
         'login_required',
         'set_link',
-        )
+    )  # type: Tuple[str, ...]
 
     expired_cookie = None
 
@@ -836,10 +841,10 @@ class TestBugzillaAPIXMLRPCTransport(TestBugzillaXMLRPCTransport):
         }
 
     # Methods that require authentication.
-    auth_required_methods = [
+    auth_required_methods = (
         'add_comment',
         'login_required',
-        ]
+    )
 
     # The list of users that can log in.
     users = [
@@ -1292,8 +1297,8 @@ def strip_trac_comment(comment):
 class TestTracXMLRPCTransport(RequestsTransport):
     """An XML-RPC transport to be used when testing Trac."""
 
-    remote_bugs = {}
-    launchpad_bugs = {}
+    remote_bugs = {}  # type: Dict[str, Dict[str, Any]]
+    launchpad_bugs = {}  # type: Dict[str, int]
     seconds_since_epoch = None
     local_timezone = 'UTC'
     utc_offset = 0
diff --git a/lib/lp/bugs/tests/test_buglinktarget.py b/lib/lp/bugs/tests/test_buglinktarget.py
index 9c724a3..fbfac7f 100644
--- a/lib/lp/bugs/tests/test_buglinktarget.py
+++ b/lib/lp/bugs/tests/test_buglinktarget.py
@@ -7,8 +7,6 @@ This module will run the interface test against the CVE, Specification,
 Question, and BranchMergeProposal implementations of that interface.
 """
 
-__all__ = []
-
 import unittest
 
 from zope.component import getUtility
diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py
index d24c992..ed4a8da 100644
--- a/lib/lp/bugs/tests/test_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugnomination.py
@@ -203,8 +203,8 @@ class CanBeNominatedForTestMixin:
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         login('foo.bar@xxxxxxxxxxxxx')
         self.eric = self.factory.makePerson(name='eric')
         self.setUpTarget()
diff --git a/lib/lp/bugs/tests/test_bugsearch_conjoined.py b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
index 1407278..75a1979 100644
--- a/lib/lp/bugs/tests/test_bugsearch_conjoined.py
+++ b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
@@ -3,8 +3,6 @@
 
 """Test for the exclude_conjoined_tasks param for BugTaskSearchParams."""
 
-__all__ = []
-
 from storm.store import Store
 from testtools.matchers import Equals
 from zope.component import getUtility
diff --git a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
index 0233e02..8235450 100644
--- a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
@@ -23,8 +23,8 @@ class AddNominationTestMixin:
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         login('foo.bar@xxxxxxxxxxxxx')
         self.user = self.factory.makePerson(name='ordinary-user')
         self.bug_supervisor = self.factory.makePerson(name='no-ordinary-user')
diff --git a/lib/lp/bugs/tests/test_bugtarget.py b/lib/lp/bugs/tests/test_bugtarget.py
index 79fa9a1..a5df3f5 100644
--- a/lib/lp/bugs/tests/test_bugtarget.py
+++ b/lib/lp/bugs/tests/test_bugtarget.py
@@ -8,8 +8,6 @@ ProjectGroup, DistributionSourcePackage, and DistroSeries implementations
 IBugTarget. It runs the bugtarget-questiontarget.rst test.
 """
 
-__all__ = []
-
 import random
 import unittest
 
diff --git a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
index 0b77ce3..7f00680 100644
--- a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
+++ b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
@@ -1,8 +1,11 @@
 # Copyright 2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from collections import namedtuple
 from contextlib import contextmanager
+from typing import (
+    Any,
+    NamedTuple,
+    )
 
 from testtools.matchers import MatchesStructure
 from zope.component import getUtility
@@ -27,35 +30,32 @@ from lp.testing.dbuser import dbuser
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
-BUGTASKFLAT_COLUMNS = (
-    'bugtask',
-    'bug',
-    'datecreated',
-    'latest_patch_uploaded',
-    'date_closed',
-    'date_last_updated',
-    'duplicateof',
-    'bug_owner',
-    'fti',
-    'information_type',
-    'heat',
-    'product',
-    'productseries',
-    'distribution',
-    'distroseries',
-    'sourcepackagename',
-    'status',
-    'importance',
-    'assignee',
-    'milestone',
-    'owner',
-    'active',
-    'access_policies',
-    'access_grants',
-    )
-
-BugTaskFlat = namedtuple('BugTaskFlat', BUGTASKFLAT_COLUMNS)
-
+BugTaskFlat = NamedTuple('BugTaskFlat', (
+    ('bugtask', Any),
+    ('bug', Any),
+    ('datecreated', Any),
+    ('latest_patch_uploaded', Any),
+    ('date_closed', Any),
+    ('date_last_updated', Any),
+    ('duplicateof', Any),
+    ('bug_owner', Any),
+    ('fti', Any),
+    ('information_type', Any),
+    ('heat', Any),
+    ('product', Any),
+    ('productseries', Any),
+    ('distribution', Any),
+    ('distroseries', Any),
+    ('sourcepackagename', Any),
+    ('status', Any),
+    ('importance', Any),
+    ('assignee', Any),
+    ('milestone', Any),
+    ('owner', Any),
+    ('active', Any),
+    ('access_policies', Any),
+    ('access_grants', Any),
+))
 
 class BugTaskFlatTestMixin(TestCaseWithFactory):
 
@@ -84,7 +84,7 @@ class BugTaskFlatTestMixin(TestCaseWithFactory):
         assert bugtask is not None
         result = IStore(Bug).execute(
             "SELECT %s FROM bugtaskflat WHERE bugtask = ?"
-            % ', '.join(BUGTASKFLAT_COLUMNS), (bugtask,)).get_one()
+            % ', '.join(BugTaskFlat._fields), (bugtask,)).get_one()
         if result is not None:
             result = BugTaskFlat(*result)
         return result
diff --git a/lib/lp/bugs/tests/test_bugtracker_components.py b/lib/lp/bugs/tests/test_bugtracker_components.py
index f457d7c..7735f5c 100644
--- a/lib/lp/bugs/tests/test_bugtracker_components.py
+++ b/lib/lp/bugs/tests/test_bugtracker_components.py
@@ -3,8 +3,6 @@
 
 """Test for components and component groups (products) in bug trackers."""
 
-__all__ = []
-
 import transaction
 
 from lp.testing import (
diff --git a/lib/lp/bugs/tests/test_bugwatch.py b/lib/lp/bugs/tests/test_bugwatch.py
index 9a363d5..9b3af1d 100644
--- a/lib/lp/bugs/tests/test_bugwatch.py
+++ b/lib/lp/bugs/tests/test_bugwatch.py
@@ -8,6 +8,10 @@ from datetime import (
     timedelta,
     )
 import re
+from typing import (
+    List,
+    Optional,
+    )
 from urllib.parse import urlunsplit
 
 from lazr.lifecycle.snapshot import Snapshot
@@ -186,16 +190,16 @@ class ExtractBugTrackerAndBugTest(WithScenarios, TestCase):
     layer = LaunchpadFunctionalLayer
 
     # A URL to an unregistered bug tracker.
-    base_url = None
+    base_url = None  # type: str
 
     # The bug tracker type to be tested.
     bugtracker_type = None
 
     # A sample URL to a bug in the bug tracker.
-    bug_url = None
+    bug_url = None  # type: str
 
     # The bug id in the sample bug_url.
-    bug_id = None
+    bug_id = None  # type: Optional[str]
 
     # True if the bug tracker is already registered in sampledata.
     already_registered = False
@@ -324,7 +328,7 @@ class EmailAddressExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTest):
     """Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.
     """
 
-    scenarios = None
+    scenarios = []  # type: List
     bugtracker_type = BugTrackerType.EMAILADDRESS
     bug_url = 'mailto:foo.bar@xxxxxxxxxxx'
     base_url = 'mailto:foo.bar@xxxxxxxxxxx'
diff --git a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
index 71c6801..2032b2d 100644
--- a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
+++ b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
@@ -3,8 +3,6 @@
 
 """Tests cronscript for retriving components from remote Bugzillas"""
 
-__all__ = []
-
 import os
 import re
 
diff --git a/lib/lp/bugs/tests/test_externalbugtracker.py b/lib/lp/bugs/tests/test_externalbugtracker.py
index 9805765..bb2ed10 100644
--- a/lib/lp/bugs/tests/test_externalbugtracker.py
+++ b/lib/lp/bugs/tests/test_externalbugtracker.py
@@ -3,8 +3,6 @@
 
 """Test related to ExternalBugtracker test infrastructure."""
 
-__all__ = []
-
 import unittest
 
 from lp.testing.layers import LaunchpadFunctionalLayer
diff --git a/lib/lp/bugs/tests/test_structuralsubscription.py b/lib/lp/bugs/tests/test_structuralsubscription.py
index 05464c9..b53168d 100644
--- a/lib/lp/bugs/tests/test_structuralsubscription.py
+++ b/lib/lp/bugs/tests/test_structuralsubscription.py
@@ -145,8 +145,8 @@ class FilteredStructuralSubscriptionTestBase:
     def makeBugTask(self):
         return self.factory.makeBugTask(target=self.target)
 
-    def setUp(self):
-        super().setUp()
+    def setUp(self, *args, **kwargs):
+        super().setUp(*args, **kwargs)
         self.ordinary_subscriber = self.factory.makePerson()
         login_person(self.ordinary_subscriber)
         self.target = self.makeTarget()
diff --git a/lib/lp/bugs/tests/test_yuitests.py b/lib/lp/bugs/tests/test_yuitests.py
index 88958ca..69598a4 100644
--- a/lib/lp/bugs/tests/test_yuitests.py
+++ b/lib/lp/bugs/tests/test_yuitests.py
@@ -3,8 +3,6 @@
 
 """Run YUI.test tests."""
 
-__all__ = []
-
 from lp.testing import (
     build_yui_unittest_suite,
     YUIUnitTestCase,
diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py
index 9001fdb..98d49c1 100644
--- a/lib/lp/registry/browser/product.py
+++ b/lib/lp/registry/browser/product.py
@@ -40,6 +40,7 @@ __all__ = [
 
 
 from operator import attrgetter
+from typing import Type
 from urllib.parse import urlunsplit
 
 from breezy import urlutils
@@ -1366,7 +1367,7 @@ class ProductBrandingView(BrandingChangeView):
 
 @implementer(IProductEditMenu)
 class ProductConfigureBase(ReturnToReferrerMixin, LaunchpadEditFormView):
-    schema = IProduct
+    schema = IProduct  # type: Type[Interface]
     usage_fieldname = None
 
     def setUpFields(self):
diff --git a/lib/lp/services/database/sqlobject/__init__.py b/lib/lp/services/database/sqlobject/__init__.py
index 381c7d2..3bafb8b 100644
--- a/lib/lp/services/database/sqlobject/__init__.py
+++ b/lib/lp/services/database/sqlobject/__init__.py
@@ -7,7 +7,29 @@
 import datetime
 
 from storm.expr import SQL
-from storm.sqlobject import *  # noqa: F401,F403
+from storm.sqlobject import AND  # noqa: F401
+from storm.sqlobject import BoolCol  # noqa: F401
+from storm.sqlobject import CONTAINSSTRING  # noqa: F401
+from storm.sqlobject import DateCol  # noqa: F401
+from storm.sqlobject import DESC  # noqa: F401
+from storm.sqlobject import FloatCol  # noqa: F401
+from storm.sqlobject import ForeignKey  # noqa: F401
+from storm.sqlobject import IN  # noqa: F401
+from storm.sqlobject import IntCol  # noqa: F401
+from storm.sqlobject import IntervalCol  # noqa: F401
+from storm.sqlobject import LIKE  # noqa: F401
+from storm.sqlobject import NOT  # noqa: F401
+from storm.sqlobject import OR  # noqa: F401
+from storm.sqlobject import SingleJoin  # noqa: F401
+from storm.sqlobject import SQLConstant  # noqa: F401
+from storm.sqlobject import SQLMultipleJoin  # noqa: F401
+from storm.sqlobject import SQLObjectBase  # noqa: F401
+from storm.sqlobject import SQLObjectMoreThanOneResultError  # noqa: F401
+from storm.sqlobject import SQLObjectNotFound  # noqa: F401
+from storm.sqlobject import SQLObjectResultSet  # noqa: F401
+from storm.sqlobject import SQLRelatedJoin  # noqa: F401
+from storm.sqlobject import StringCol  # noqa: F401
+from storm.sqlobject import UtcDateTimeCol  # noqa: F401
 
 
 _sqlStringReplace = [
diff --git a/lib/lp/services/feeds/browser.py b/lib/lp/services/feeds/browser.py
index 19e76fe..45f2b9e 100644
--- a/lib/lp/services/feeds/browser.py
+++ b/lib/lp/services/feeds/browser.py
@@ -21,6 +21,11 @@ __all__ = [
     'RootAnnouncementsFeedLink',
     ]
 
+from typing import (
+    Tuple,
+    Type,
+    )
+
 from zope.component import getUtility
 from zope.interface import implementer
 from zope.publisher.interfaces import NotFound
@@ -366,7 +371,7 @@ class FeedsMixin:
         ProjectBranchesFeedLink,
         ProjectRevisionsFeedLink,
         RootAnnouncementsFeedLink,
-        )
+    )  # type: Tuple[Type[FeedLinkBase, ...]]
 
     @property
     def feed_links(self):
diff --git a/lib/lp/services/looptuner.py b/lib/lp/services/looptuner.py
index a3db4b8..27d1d50 100644
--- a/lib/lp/services/looptuner.py
+++ b/lib/lp/services/looptuner.py
@@ -368,7 +368,7 @@ class TunableLoop:
 
     goal_seconds = 2
     minimum_chunk_size = 1
-    maximum_chunk_size = None  # Override.
+    maximum_chunk_size = None  # type: int
     cooldown_time = 0
 
     def __init__(self, log, abort_time=None):
diff --git a/lib/lp/services/mail/commands.py b/lib/lp/services/mail/commands.py
index 038f8b4..fd37fe6 100644
--- a/lib/lp/services/mail/commands.py
+++ b/lib/lp/services/mail/commands.py
@@ -60,7 +60,7 @@ class EmailCommand:
 
     Both name the values in the args list are strings.
     """
-    _numberOfArguments = None
+    _numberOfArguments = None  # type: int
 
     # Should command arguments be converted to lowercase?
     case_insensitive_args = True
diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py
index e7acbdb..f76f023 100644
--- a/lib/lp/services/scripts/base.py
+++ b/lib/lp/services/scripts/base.py
@@ -149,8 +149,8 @@ class LaunchpadScript:
     """
     lock = None
     txn = None
-    usage = None
-    description = None
+    usage = ""
+    description = ""
     lockfilepath = None
     loglevel = logging.INFO
 
diff --git a/lib/lp/services/webapp/breadcrumb.py b/lib/lp/services/webapp/breadcrumb.py
index eae2074..0699248 100644
--- a/lib/lp/services/webapp/breadcrumb.py
+++ b/lib/lp/services/webapp/breadcrumb.py
@@ -26,7 +26,7 @@ class Breadcrumb:
     This class is intended for use as an adapter.
     """
 
-    text = None
+    text = None  # type: str
     _detail = None
     _url = None
     inside = None
diff --git a/lib/lp/services/webapp/menu.py b/lib/lp/services/webapp/menu.py
index 627502a..6783750 100644
--- a/lib/lp/services/webapp/menu.py
+++ b/lib/lp/services/webapp/menu.py
@@ -19,6 +19,7 @@ __all__ = [
     ]
 
 import types
+from typing import List
 
 from lazr.delegates import delegate_to
 from lazr.restful.utils import get_current_browser_request
@@ -192,7 +193,7 @@ MENU_ANNOTATION_KEY = 'lp.services.webapp.menu.links'
 class MenuBase(UserAttributeCache):
     """Base class for facets and menus."""
 
-    links = None
+    links = None  # type: List[str]
     extra_attributes = None
     enable_only = ALL_LINKS
     _baseclassname = 'MenuBase'
@@ -381,7 +382,7 @@ class NavigationMenu(MenuBase):
 
     _baseclassname = 'NavigationMenu'
 
-    title = None
+    title = None  # type: str
     disabled = False
 
     def initLink(self, linkname, request_url):
diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
index 9e01c04..ee7b1fd 100644
--- a/lib/lp/services/webapp/publisher.py
+++ b/lib/lp/services/webapp/publisher.py
@@ -28,6 +28,12 @@ __all__ = [
 from cgi import FieldStorage
 import http.client
 import re
+from typing import (
+    Any,
+    Dict,
+    Optional,
+    Type,
+    )
 from urllib.parse import urlparse
 from wsgiref.headers import Headers
 
@@ -529,7 +535,7 @@ class LaunchpadView(UserAttributeCache):
         return None
 
     # Names of feature flags which affect a view.
-    related_features = ()
+    related_features = {}  # type: Dict[str, bool]
 
     @property
     def related_feature_info(self):
@@ -888,7 +894,7 @@ class Navigation:
         self.request = request
 
     # Set this if you want to set a new layer before doing any traversal.
-    newlayer = None
+    newlayer = None  # type: Optional[Type[Any]]
 
     def traverse(self, name):
         """Override this method to handle traversal.
diff --git a/lib/lp/services/webapp/vocabulary.py b/lib/lp/services/webapp/vocabulary.py
index 023deca..4eb92d4 100644
--- a/lib/lp/services/webapp/vocabulary.py
+++ b/lib/lp/services/webapp/vocabulary.py
@@ -22,6 +22,7 @@ __all__ = [
 ]
 
 from collections import namedtuple
+from typing import Optional
 
 import six
 from storm.base import Storm
@@ -289,8 +290,8 @@ class SQLObjectVocabularyBase(FilteredVocabularyBase):
     Then the vocabulary for the widget that captures a value for bar
     should derive from SQLObjectVocabularyBase.
     """
-    _orderBy = None
-    _filter = None
+    _orderBy = None  # type: Optional[str]
+    _filter = None  # type: Optional[bool]
     _clauseTables = None
 
     def __init__(self, context=None):
diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
index bfc08cc..0f42d27 100644
--- a/lib/lp/testing/__init__.py
+++ b/lib/lp/testing/__init__.py
@@ -68,6 +68,10 @@ import subprocess
 import sys
 import tempfile
 import time
+from typing import (
+    Type,
+    TYPE_CHECKING,
+    )
 import unittest
 
 from breezy import trace
@@ -176,6 +180,9 @@ from lp.testing.karma import KarmaRecorder
 from lp.testing.mail_helpers import pop_notifications
 
 
+if TYPE_CHECKING:
+    from lp.testing.layers import BaseLayer
+
 # The following names have been imported for the purpose of being
 # exported. They are referred to here to silence lint warnings.
 admin_logged_in
@@ -1032,7 +1039,7 @@ class WebServiceTestCase(TestCaseWithFactory):
 
 class AbstractYUITestCase(TestCase):
 
-    layer = None
+    layer = None  # type: Type[BaseLayer]
     suite_name = ''
     # 30 seconds for the suite.
     suite_timeout = 30000
diff --git a/pyproject.toml b/pyproject.toml
index 1f331da..b45aa38 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,102 @@
 [tool.black]
 line-length = 79
 target-version = ['py35']
+
+[tool.mypy]
+python_version = "3.5"
+exclude = [
+    '/interfaces/',
+    'interfaces\.py$',
+]
+
+[[tool.mypy.overrides]]
+module = "zope.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "twisted.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "storm.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "lazr.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "testtools.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "responses"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "transaction"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "soupmatchers"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "defusedxml.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "testscenarios.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "pystache"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "fixtures"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "breezy.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "feedparser"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "apt_inst"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "apt_pkg"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "debian.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "treq"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "gpgme"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "artifactory"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "dohq_artifactory.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "pymacaroons"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "iso8601"
+ignore_missing_imports = true
diff --git a/requirements/types.txt b/requirements/types.txt
new file mode 100644
index 0000000..d9a338e
--- /dev/null
+++ b/requirements/types.txt
@@ -0,0 +1,7 @@
+types-pytz==0.1.0
+types-simplejson==0.1.0
+types-six==0.1.9
+types-beautifulsoup4==4.9.0
+types-requests==0.1.13
+lxml-stubs==0.4.0
+types-Markdown==0.1.0