From ed38ce579002bf915442cd32ec9406c6c54fa397 Mon Sep 17 00:00:00 2001 From: Deimos Date: Wed, 14 Jul 2021 21:22:52 -0600 Subject: [PATCH] Type annotations: use standard generics (PEP 585) As of Python 3.9, it's no longer necessary to import things like List and Dict from the typing module, and we can just use the built-in types like this. --- .../post_processing_script_runner.py | 4 +-- tildes/consumers/site_icon_downloader.py | 3 +- tildes/consumers/topic_embedly_extractor.py | 2 +- tildes/consumers/topic_metadata_generator.py | 7 +++-- tildes/consumers/topic_youtube_scraper.py | 2 +- tildes/tildes/__init__.py | 4 +-- tildes/tildes/auth.py | 3 +- tildes/tildes/database.py | 4 +-- tildes/tildes/enums.py | 4 +-- tildes/tildes/lib/auth.py | 4 +-- tildes/tildes/lib/database.py | 11 +++---- tildes/tildes/lib/event_stream.py | 9 +++--- tildes/tildes/lib/lua.py | 3 +- tildes/tildes/lib/markdown.py | 30 +++++++------------ tildes/tildes/lib/ratelimit.py | 7 +++-- tildes/tildes/lib/site_info.py | 4 +-- tildes/tildes/lib/string.py | 7 +++-- tildes/tildes/metrics.py | 2 +- tildes/tildes/models/comment/comment.py | 3 +- .../models/comment/comment_notification.py | 7 ++--- tildes/tildes/models/comment/comment_tree.py | 13 ++++---- tildes/tildes/models/database_model.py | 6 ++-- tildes/tildes/models/group/group.py | 8 ++--- tildes/tildes/models/group/group_wiki_page.py | 4 +-- tildes/tildes/models/log/log.py | 8 ++--- tildes/tildes/models/message/message.py | 5 ++-- tildes/tildes/models/model_query.py | 3 +- tildes/tildes/models/pagination.py | 9 +++--- tildes/tildes/models/topic/topic.py | 15 +++++----- tildes/tildes/models/topic/topic_query.py | 3 +- tildes/tildes/models/topic/topic_schedule.py | 6 ++-- tildes/tildes/models/user/user.py | 8 ++--- tildes/tildes/request_methods.py | 10 +++---- tildes/tildes/schemas/fields.py | 7 +++-- tildes/tildes/schemas/topic.py | 5 ++-- tildes/tildes/scrapers/embedly_scraper.py | 6 ++-- tildes/tildes/scrapers/youtube_scraper.py | 6 ++-- tildes/tildes/tweens.py | 2 +- tildes/tildes/typing.py | 6 ++-- tildes/tildes/views/api/web/exceptions.py | 2 +- tildes/tildes/views/bookmarks.py | 4 +-- tildes/tildes/views/decorators.py | 5 ++-- tildes/tildes/views/exceptions.py | 2 +- tildes/tildes/views/financials.py | 6 ++-- tildes/tildes/views/user.py | 12 ++++---- tildes/tildes/views/votes.py | 4 +-- 46 files changed, 145 insertions(+), 140 deletions(-) diff --git a/tildes/consumers/post_processing_script_runner.py b/tildes/consumers/post_processing_script_runner.py index 963ea5d..81195df 100644 --- a/tildes/consumers/post_processing_script_runner.py +++ b/tildes/consumers/post_processing_script_runner.py @@ -3,7 +3,7 @@ """Consumer that runs processing scripts on posts.""" -from typing import Type, Union +from typing import Union from sqlalchemy import desc from sqlalchemy.sql.expression import or_ @@ -23,7 +23,7 @@ class PostProcessingScriptRunner(EventStreamConsumer): def process_message(self, message: Message) -> None: """Process a message from the stream.""" - wrapper_class: Union[Type[TopicScriptingWrapper], Type[CommentScriptingWrapper]] + wrapper_class: Union[type[TopicScriptingWrapper], type[CommentScriptingWrapper]] if "topic_id" in message.fields: post = ( diff --git a/tildes/consumers/site_icon_downloader.py b/tildes/consumers/site_icon_downloader.py index 83af260..2b0b795 100644 --- a/tildes/consumers/site_icon_downloader.py +++ b/tildes/consumers/site_icon_downloader.py @@ -3,9 +3,10 @@ """Consumer that downloads site icons using Embedly scraper data.""" +from collections.abc import Sequence from io import BytesIO from os import path -from typing import Optional, Sequence +from typing import Optional import publicsuffix import requests diff --git a/tildes/consumers/topic_embedly_extractor.py b/tildes/consumers/topic_embedly_extractor.py index 93b6fa6..087a371 100644 --- a/tildes/consumers/topic_embedly_extractor.py +++ b/tildes/consumers/topic_embedly_extractor.py @@ -4,8 +4,8 @@ """Consumer that fetches data from Embedly's Extract API for link topics.""" import os +from collections.abc import Sequence from datetime import timedelta -from typing import Sequence from pyramid.paster import get_appsettings from requests.exceptions import HTTPError, Timeout diff --git a/tildes/consumers/topic_metadata_generator.py b/tildes/consumers/topic_metadata_generator.py index ff5f6a7..7ea4842 100644 --- a/tildes/consumers/topic_metadata_generator.py +++ b/tildes/consumers/topic_metadata_generator.py @@ -3,7 +3,8 @@ """Consumer that generates content_metadata for topics.""" -from typing import Any, Dict, Sequence +from collections.abc import Sequence +from typing import Any from ipaddress import ip_address import publicsuffix @@ -61,7 +62,7 @@ class TopicMetadataGenerator(EventStreamConsumer): ) @staticmethod - def _generate_text_metadata(topic: Topic) -> Dict[str, Any]: + def _generate_text_metadata(topic: Topic) -> dict[str, Any]: """Generate metadata for a text topic (word count and excerpt).""" if not topic.rendered_html: return {} @@ -81,7 +82,7 @@ class TopicMetadataGenerator(EventStreamConsumer): except ValueError: return False - def _generate_link_metadata(self, topic: Topic) -> Dict[str, Any]: + def _generate_link_metadata(self, topic: Topic) -> dict[str, Any]: """Generate metadata for a link topic (domain).""" if not topic.link: return {} diff --git a/tildes/consumers/topic_youtube_scraper.py b/tildes/consumers/topic_youtube_scraper.py index 1c576e9..c00731e 100644 --- a/tildes/consumers/topic_youtube_scraper.py +++ b/tildes/consumers/topic_youtube_scraper.py @@ -4,8 +4,8 @@ """Consumer that fetches data from YouTube's data API for relevant link topics.""" import os +from collections.abc import Sequence from datetime import timedelta -from typing import Sequence from pyramid.paster import get_appsettings from requests.exceptions import HTTPError, Timeout diff --git a/tildes/tildes/__init__.py b/tildes/tildes/__init__.py index c4d62cc..829232e 100644 --- a/tildes/tildes/__init__.py +++ b/tildes/tildes/__init__.py @@ -3,8 +3,6 @@ """Configure and initialize the Pyramid app.""" -from typing import Dict - import sentry_sdk from marshmallow.exceptions import ValidationError from paste.deploy.config import PrefixMiddleware @@ -13,7 +11,7 @@ from sentry_sdk.integrations.pyramid import PyramidIntegration from webassets import Bundle -def main(global_config: Dict[str, str], **settings: str) -> PrefixMiddleware: +def main(global_config: dict[str, str], **settings: str) -> PrefixMiddleware: """Configure and return a Pyramid WSGI application.""" config = Configurator(settings=settings) diff --git a/tildes/tildes/auth.py b/tildes/tildes/auth.py index b94929c..6b72f63 100644 --- a/tildes/tildes/auth.py +++ b/tildes/tildes/auth.py @@ -3,7 +3,8 @@ """Configuration and functionality related to authentication/authorization.""" -from typing import Any, Optional, Sequence +from collections.abc import Sequence +from typing import Any, Optional from pyramid.authentication import SessionAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy diff --git a/tildes/tildes/database.py b/tildes/tildes/database.py index c1cf4b4..39d2732 100644 --- a/tildes/tildes/database.py +++ b/tildes/tildes/database.py @@ -3,7 +3,7 @@ """Contains the database-related config updates and request methods.""" -from typing import Callable, Type +from collections.abc import Callable from pyramid.config import Configurator from pyramid.request import Request @@ -31,7 +31,7 @@ def obtain_lock(request: Request, lock_space: str, lock_value: int) -> None: obtain_transaction_lock(request.db_session, lock_space, lock_value) -def query_factory(request: Request, model_cls: Type[DatabaseModel]) -> ModelQuery: +def query_factory(request: Request, model_cls: type[DatabaseModel]) -> ModelQuery: """Return a ModelQuery or subclass depending on model_cls specified.""" if model_cls == Comment: return CommentQuery(request) diff --git a/tildes/tildes/enums.py b/tildes/tildes/enums.py index a17945a..cb5792a 100644 --- a/tildes/tildes/enums.py +++ b/tildes/tildes/enums.py @@ -5,7 +5,7 @@ import enum from datetime import timedelta -from typing import Any, List, Optional +from typing import Any, Optional from tildes.lib.datetime import utc_from_timestamp @@ -109,7 +109,7 @@ class ContentMetadataFields(enum.Enum): def detail_fields_for_content_type( cls, content_type: "TopicContentType", - ) -> List["ContentMetadataFields"]: + ) -> list["ContentMetadataFields"]: """Return a list of fields to display for detail about a particular type.""" if content_type is TopicContentType.ARTICLE: return [cls.WORD_COUNT, cls.PUBLISHED] diff --git a/tildes/tildes/lib/auth.py b/tildes/tildes/lib/auth.py index c59e431..16c1875 100644 --- a/tildes/tildes/lib/auth.py +++ b/tildes/tildes/lib/auth.py @@ -3,7 +3,7 @@ """Functions to help with authorization, such as generating ACLs.""" -from typing import List, Optional +from typing import Optional from pyramid.security import Allow, Deny @@ -14,7 +14,7 @@ def aces_for_permission( required_permission: str, group_id: Optional[int] = None, granted_permission: Optional[str] = None, -) -> List[AceType]: +) -> list[AceType]: """Return the ACEs for manually-granted (or denied) entries in UserPermissions.""" aces = [] diff --git a/tildes/tildes/lib/database.py b/tildes/tildes/lib/database.py index b0d7eab..e3d17bc 100644 --- a/tildes/tildes/lib/database.py +++ b/tildes/tildes/lib/database.py @@ -4,7 +4,8 @@ """Constants/classes/functions related to the database.""" import enum -from typing import Any, Callable, List, Optional +from collections.abc import Callable +from typing import Any, Optional from dateutil.rrule import rrule, rrulestr from pyramid.paster import bootstrap @@ -106,7 +107,7 @@ class ArrayOfLtree(ARRAY): """Return a conversion function for processing result row values.""" super_rp = super().result_processor(dialect, coltype) - def handle_raw_string(value: str) -> List[str]: + def handle_raw_string(value: str) -> list[str]: if not (value.startswith("{") and value.endswith("}")): raise ValueError("%s is not an array value" % value) @@ -119,7 +120,7 @@ class ArrayOfLtree(ARRAY): return value.split(",") - def process(value: Optional[str]) -> Optional[List[str]]: + def process(value: Optional[str]) -> Optional[list[str]]: if value is None: return None @@ -183,10 +184,10 @@ class TagList(TypeDecorator): impl = ArrayOfLtree - def process_bind_param(self, value: str, dialect: Dialect) -> List[Ltree]: + def process_bind_param(self, value: str, dialect: Dialect) -> list[Ltree]: """Convert the value to ltree[] for storing.""" return [Ltree(tag.replace(" ", "_")) for tag in value] - def process_result_value(self, value: List[Ltree], dialect: Dialect) -> List[str]: + def process_result_value(self, value: list[Ltree], dialect: Dialect) -> list[str]: """Convert the stored value to a list of strings.""" return [str(tag).replace("_", " ") for tag in value] diff --git a/tildes/tildes/lib/event_stream.py b/tildes/tildes/lib/event_stream.py index 6dfa41b..e752dbc 100644 --- a/tildes/tildes/lib/event_stream.py +++ b/tildes/tildes/lib/event_stream.py @@ -5,8 +5,9 @@ import os from abc import abstractmethod +from collections.abc import Sequence from configparser import ConfigParser -from typing import Any, Dict, List, Optional, Sequence +from typing import Any, Optional from prometheus_client import CollectorRegistry, Counter, start_http_server from redis import Redis, ResponseError @@ -22,7 +23,7 @@ class Message: """Represents a single message taken from a stream.""" def __init__( - self, redis: Redis, stream: str, message_id: str, fields: Dict[str, str] + self, redis: Redis, stream: str, message_id: str, fields: dict[str, str] ): """Initialize a new message from a Redis stream.""" self.redis = redis @@ -181,7 +182,7 @@ class EventStreamConsumer: message_ids=[entry["message_id"]], ) - def _xreadgroup_response_to_messages(self, response: Any) -> List[Message]: + def _xreadgroup_response_to_messages(self, response: Any) -> list[Message]: """Convert a response from XREADGROUP to a list of Messages.""" messages = [] @@ -204,7 +205,7 @@ class EventStreamConsumer: return messages - def _get_messages(self, pending: bool = False) -> List[Message]: + def _get_messages(self, pending: bool = False) -> list[Message]: """Get any messages from the streams for this consumer. This method will return at most one message from each of the source streams per diff --git a/tildes/tildes/lib/lua.py b/tildes/tildes/lib/lua.py index 27d420e..fc3cf52 100644 --- a/tildes/tildes/lib/lua.py +++ b/tildes/tildes/lib/lua.py @@ -3,8 +3,9 @@ """Functions and classes related to Lua scripting.""" +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any, Optional from lupa import LuaError, LuaRuntime diff --git a/tildes/tildes/lib/markdown.py b/tildes/tildes/lib/markdown.py index be3b232..f2dd4c8 100644 --- a/tildes/tildes/lib/markdown.py +++ b/tildes/tildes/lib/markdown.py @@ -4,19 +4,9 @@ """Functions/constants related to markdown handling.""" import re +from collections.abc import Callable, Iterator from functools import partial -from typing import ( - Any, - Callable, - Dict, - Iterator, - List, - Match, - Optional, - Pattern, - Tuple, - Union, -) +from typing import Any, Optional, Union import bleach from bs4 import BeautifulSoup @@ -105,7 +95,7 @@ ALLOWED_HTML_TAGS = ( ) ALLOWED_LINK_PROTOCOLS = ("gemini", "http", "https", "mailto") -ALLOWED_HTML_ATTRIBUTES_DEFAULT: Dict[str, Union[List[str], Callable]] = { +ALLOWED_HTML_ATTRIBUTES_DEFAULT: dict[str, Union[list[str], Callable]] = { "a": ["href", "title"], "details": ["open"], "ol": ["start"], @@ -234,7 +224,7 @@ class CodeHtmlFormatter(HtmlFormatter): ... instead (assumes a
 is already present).
     """
 
-    def wrap(self, source: Any, outfile: Any) -> Iterator[Tuple[int, str]]:
+    def wrap(self, source: Any, outfile: Any) -> Iterator[tuple[int, str]]:
         """Wrap the highlighted tokens with the  tag."""
         # pylint: disable=unused-argument
         yield (0, '')
@@ -311,7 +301,7 @@ class LinkifyFilter(Filter):
     SUBREDDIT_REFERENCE_REGEX = re.compile(r"(? List[dict]:
+        tokens: list[dict], filter_regex: re.Pattern, linkify_function: Callable
+    ) -> list[dict]:
         """Check tokens for text that matches a regex and linkify it.
 
         The `filter_regex` argument should be a compiled pattern that will be applied to
@@ -434,7 +424,7 @@ class LinkifyFilter(Filter):
         return new_tokens
 
     @staticmethod
-    def _tokenize_group_match(match: Match) -> List[dict]:
+    def _tokenize_group_match(match: re.Match) -> list[dict]:
         """Convert a potential group reference into HTML tokens."""
         # convert the potential group path to lowercase to allow people to use incorrect
         # casing but still have it link properly
@@ -465,7 +455,7 @@ class LinkifyFilter(Filter):
         return [{"type": "Characters", "data": match[0]}]
 
     @staticmethod
-    def _tokenize_username_match(match: Match) -> List[dict]:
+    def _tokenize_username_match(match: re.Match) -> list[dict]:
         """Convert a potential username reference into HTML tokens."""
         # if it's a valid username, convert to 
         if is_valid_username(match[1]):
@@ -486,7 +476,7 @@ class LinkifyFilter(Filter):
         return [{"type": "Characters", "data": match[0]}]
 
     @staticmethod
-    def _tokenize_subreddit_match(match: Match) -> List[dict]:
+    def _tokenize_subreddit_match(match: re.Match) -> list[dict]:
         """Convert a subreddit reference into HTML tokens."""
         return [
             {
diff --git a/tildes/tildes/lib/ratelimit.py b/tildes/tildes/lib/ratelimit.py
index 9bd7002..fb38765 100644
--- a/tildes/tildes/lib/ratelimit.py
+++ b/tildes/tildes/lib/ratelimit.py
@@ -4,9 +4,10 @@
 """Classes and constants related to rate-limited actions."""
 
 from __future__ import annotations
+from collections.abc import Sequence
 from datetime import timedelta
 from ipaddress import ip_address
-from typing import Any, List, Optional, Sequence
+from typing import Any, Optional
 
 from pyramid.response import Response
 from redis import Redis
@@ -69,7 +70,7 @@ class RateLimitResult:
         )
 
     @classmethod
-    def from_redis_cell_result(cls, result: List[int]) -> RateLimitResult:
+    def from_redis_cell_result(cls, result: list[int]) -> RateLimitResult:
         """Convert the response from CL.THROTTLE command to a RateLimitResult.
 
         CL.THROTTLE responds with an array of 5 integers:
@@ -228,7 +229,7 @@ class RateLimitedAction:
 
         return ":".join(parts)
 
-    def _call_redis_command(self, key: str) -> List[int]:
+    def _call_redis_command(self, key: str) -> list[int]:
         """Call the redis-cell CL.THROTTLE command for this action."""
         return self.redis.execute_command(
             "CL.THROTTLE",
diff --git a/tildes/tildes/lib/site_info.py b/tildes/tildes/lib/site_info.py
index c4acc48..c48faac 100644
--- a/tildes/tildes/lib/site_info.py
+++ b/tildes/tildes/lib/site_info.py
@@ -3,7 +3,7 @@
 
 """Library code related to displaying info about individual websites."""
 
-from typing import List, Optional
+from typing import Optional
 
 from tildes.enums import ContentMetadataFields, TopicContentType
 
@@ -22,7 +22,7 @@ class SiteInfo:
         self.show_author = show_author
         self.content_type = content_type
 
-    def content_source(self, authors: Optional[List[str]] = None) -> str:
+    def content_source(self, authors: Optional[list[str]] = None) -> str:
         """Return a string representing the "source" of content on this site.
 
         If the site isn't one that needs to show its author, this is just its name.
diff --git a/tildes/tildes/lib/string.py b/tildes/tildes/lib/string.py
index 74e3181..51c5e62 100644
--- a/tildes/tildes/lib/string.py
+++ b/tildes/tildes/lib/string.py
@@ -5,7 +5,8 @@
 
 import re
 import unicodedata
-from typing import Iterator, List, Optional
+from collections.abc import Iterator
+from typing import Optional
 from urllib.parse import quote
 from xml.etree.ElementTree import Element
 
@@ -225,10 +226,10 @@ def separate_string(original: str, separator: str, segment_size: int) -> str:
     return separated
 
 
-def extract_text_from_html(html: str, skip_tags: Optional[List[str]] = None) -> str:
+def extract_text_from_html(html: str, skip_tags: Optional[list[str]] = None) -> str:
     """Extract plain text content from the elements inside an HTML string."""
 
-    def extract_text(element: Element, skip_tags: List[str]) -> Iterator[str]:
+    def extract_text(element: Element, skip_tags: list[str]) -> Iterator[str]:
         """Extract text recursively from elements, optionally skipping some tags.
 
         This function is Python's xml.etree.ElementTree.Element.itertext() but with the
diff --git a/tildes/tildes/metrics.py b/tildes/tildes/metrics.py
index 5b8dcd2..491f6ba 100644
--- a/tildes/tildes/metrics.py
+++ b/tildes/tildes/metrics.py
@@ -7,7 +7,7 @@
 # checks to avoid errors
 # pylint: disable=no-value-for-parameter,redundant-keyword-arg
 
-from typing import Callable
+from collections.abc import Callable
 
 from prometheus_client import Counter, Histogram, Summary
 
diff --git a/tildes/tildes/models/comment/comment.py b/tildes/tildes/models/comment/comment.py
index 65cd0f2..a4bc90b 100644
--- a/tildes/tildes/models/comment/comment.py
+++ b/tildes/tildes/models/comment/comment.py
@@ -4,8 +4,9 @@
 """Contains the Comment class."""
 
 from collections import Counter
+from collections.abc import Sequence
 from datetime import datetime, timedelta
-from typing import Any, Optional, Sequence, TYPE_CHECKING, Union
+from typing import Any, Optional, TYPE_CHECKING, Union
 
 from pyramid.security import (
     Allow,
diff --git a/tildes/tildes/models/comment/comment_notification.py b/tildes/tildes/models/comment/comment_notification.py
index 18167bd..647837e 100644
--- a/tildes/tildes/models/comment/comment_notification.py
+++ b/tildes/tildes/models/comment/comment_notification.py
@@ -5,7 +5,6 @@
 
 import re
 from datetime import datetime
-from typing import List, Tuple
 
 from pyramid.security import Allow, DENY_ALL
 from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, TIMESTAMP
@@ -120,7 +119,7 @@ class CommentNotification(DatabaseModel):
     @classmethod
     def get_mentions_for_comment(
         cls, db_session: Session, comment: Comment
-    ) -> List["CommentNotification"]:
+    ) -> list["CommentNotification"]:
         """Get a list of notifications for user mentions in the comment."""
         notifications = []
 
@@ -160,8 +159,8 @@ class CommentNotification(DatabaseModel):
     def prevent_duplicate_notifications(
         db_session: Session,
         comment: Comment,
-        new_notifications: List["CommentNotification"],
-    ) -> Tuple[List["CommentNotification"], List["CommentNotification"]]:
+        new_notifications: list["CommentNotification"],
+    ) -> tuple[list["CommentNotification"], list["CommentNotification"]]:
         """Filter new notifications for edited comments.
 
         Protect against sending a notification for the same comment to the same user
diff --git a/tildes/tildes/models/comment/comment_tree.py b/tildes/tildes/models/comment/comment_tree.py
index fbf2895..0fc4a25 100644
--- a/tildes/tildes/models/comment/comment_tree.py
+++ b/tildes/tildes/models/comment/comment_tree.py
@@ -4,8 +4,9 @@
 """Contains the CommentTree and CommentInTree classes."""
 
 from collections import Counter
+from collections.abc import Iterator, Sequence
 from datetime import datetime
-from typing import Iterator, List, Optional, Sequence, Tuple
+from typing import Optional
 
 from prometheus_client import Histogram
 from wrapt import ObjectProxy
@@ -27,7 +28,7 @@ class CommentTree:
         viewer: Optional[User] = None,
     ):
         """Create a sorted CommentTree from a flat list of Comments."""
-        self.tree: List[CommentInTree] = []
+        self.tree: list[CommentInTree] = []
         self.sort = sort
         self.viewer = viewer
 
@@ -92,7 +93,7 @@ class CommentTree:
                 self.tree.append(comment)
 
     @staticmethod
-    def _sort_tree(tree: List[Comment], sort: CommentTreeSortOption) -> List[Comment]:
+    def _sort_tree(tree: list[Comment], sort: CommentTreeSortOption) -> list[Comment]:
         """Sort the tree by the desired ordering (recursively).
 
         Because Python's sorted() function is stable, the ordering of any comments that
@@ -118,7 +119,7 @@ class CommentTree:
         return tree
 
     @staticmethod
-    def _prune_empty_branches(tree: Sequence[Comment]) -> List[Comment]:
+    def _prune_empty_branches(tree: Sequence[Comment]) -> list[Comment]:
         """Remove branches from the tree with no visible comments."""
         pruned_tree = []
 
@@ -269,7 +270,7 @@ class CommentInTree(ObjectProxy):
         super().__init__(comment)
 
         self.collapsed_state: Optional[str] = None
-        self.replies: List[CommentInTree] = []
+        self.replies: list[CommentInTree] = []
         self.has_visible_descendant = False
         self.num_children = 0
         self.depth = 0
@@ -308,7 +309,7 @@ class CommentInTree(ObjectProxy):
             reply.recursively_collapse()
 
     @property
-    def relevance_sorting_value(self) -> Tuple[int, ...]:
+    def relevance_sorting_value(self) -> tuple[int, ...]:
         """Value to use for the comment with the "relevance" comment sorting method.
 
         Returns a tuple, which allows sorting the comments into "tiers" and then still
diff --git a/tildes/tildes/models/database_model.py b/tildes/tildes/models/database_model.py
index 3b6662b..c94175c 100644
--- a/tildes/tildes/models/database_model.py
+++ b/tildes/tildes/models/database_model.py
@@ -4,7 +4,7 @@
 """Contains the base DatabaseModel class."""
 
 from datetime import timedelta
-from typing import Any, Optional, Type, TypeVar
+from typing import Any, Optional, TypeVar
 
 from marshmallow import Schema
 from sqlalchemy import event
@@ -28,7 +28,7 @@ NAMING_CONVENTION = {
 
 
 def attach_set_listener(
-    class_: Type["DatabaseModelBase"], attribute: str, instance: "DatabaseModelBase"
+    class_: type["DatabaseModelBase"], attribute: str, instance: "DatabaseModelBase"
 ) -> None:
     """Attach the SQLAlchemy ORM "set" attribute listener."""
     # pylint: disable=unused-argument
@@ -48,7 +48,7 @@ class DatabaseModelBase:
     # declare the type of __table__ so mypy understands it when checking __eq__
     __table__: Table
 
-    schema_class: Optional[Type[Schema]] = None
+    schema_class: Optional[type[Schema]] = None
 
     def __eq__(self, other: Any) -> bool:
         """Equality comparison method - check if primary key values match."""
diff --git a/tildes/tildes/models/group/group.py b/tildes/tildes/models/group/group.py
index ecd9d47..5523b90 100644
--- a/tildes/tildes/models/group/group.py
+++ b/tildes/tildes/models/group/group.py
@@ -4,7 +4,7 @@
 """Contains the Group class."""
 
 from datetime import datetime
-from typing import List, Optional
+from typing import Optional
 
 from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
 from sqlalchemy import (
@@ -67,8 +67,8 @@ class Group(DatabaseModel):
     is_user_treated_as_topic_source: bool = Column(
         Boolean, nullable=False, server_default="false"
     )
-    common_topic_tags: List[str] = Column(TagList, nullable=False, server_default="{}")
-    important_topic_tags: List[str] = Column(
+    common_topic_tags: list[str] = Column(TagList, nullable=False, server_default="{}")
+    important_topic_tags: list[str] = Column(
         TagList, nullable=False, server_default="{}"
     )
 
@@ -96,7 +96,7 @@ class Group(DatabaseModel):
             self.sidebar_rendered_html = None
 
     @property
-    def autocomplete_topic_tags(self) -> List[str]:
+    def autocomplete_topic_tags(self) -> list[str]:
         """Return the topic tags that should be offered as autocomplete options."""
         global_options = ["nsfw", "spoiler", "coronaviruses.covid19"]
 
diff --git a/tildes/tildes/models/group/group_wiki_page.py b/tildes/tildes/models/group/group_wiki_page.py
index 914a2a1..6e2404a 100644
--- a/tildes/tildes/models/group/group_wiki_page.py
+++ b/tildes/tildes/models/group/group_wiki_page.py
@@ -5,7 +5,7 @@
 
 from datetime import datetime
 from pathlib import Path, PurePath
-from typing import List, Optional
+from typing import Optional
 
 from pygit2 import Repository, Signature
 from pyramid.security import Allow, DENY_ALL, Everyone
@@ -104,7 +104,7 @@ class GroupWikiPage(DatabaseModel):
         return self.file_path.stem
 
     @property
-    def folders(self) -> List[PurePath]:
+    def folders(self) -> list[PurePath]:
         """Return a list of the folders the page is inside (if any)."""
         path = PurePath(self.path)
 
diff --git a/tildes/tildes/models/log/log.py b/tildes/tildes/models/log/log.py
index 3874414..152fad5 100644
--- a/tildes/tildes/models/log/log.py
+++ b/tildes/tildes/models/log/log.py
@@ -3,7 +3,7 @@
 
 """Contains the Log class."""
 
-from typing import Any, Dict, Optional
+from typing import Any, Optional
 
 from pyramid.request import Request
 from sqlalchemy import BigInteger, Column, event, ForeignKey, Table, TIMESTAMP
@@ -75,7 +75,7 @@ class Log(DatabaseModel, BaseLog):
         self,
         event_type: LogEventType,
         request: Request,
-        info: Optional[Dict[str, Any]] = None,
+        info: Optional[dict[str, Any]] = None,
     ):
         """Create a new log entry.
 
@@ -106,7 +106,7 @@ class LogComment(DatabaseModel, BaseLog):
         event_type: LogEventType,
         request: Request,
         comment: Comment,
-        info: Optional[Dict[str, Any]] = None,
+        info: Optional[dict[str, Any]] = None,
     ):
         """Create a new log entry related to a specific comment."""
         # pylint: disable=non-parent-init-called
@@ -135,7 +135,7 @@ class LogTopic(DatabaseModel, BaseLog):
         event_type: LogEventType,
         request: Request,
         topic: Topic,
-        info: Optional[Dict[str, Any]] = None,
+        info: Optional[dict[str, Any]] = None,
     ):
         """Create a new log entry related to a specific topic."""
         # pylint: disable=non-parent-init-called
diff --git a/tildes/tildes/models/message/message.py b/tildes/tildes/models/message/message.py
index c200489..0fa0e38 100644
--- a/tildes/tildes/models/message/message.py
+++ b/tildes/tildes/models/message/message.py
@@ -12,8 +12,9 @@ This might feel a bit unusual since it splits "all messages" across two tables/c
 but it simplifies a lot of things when organizing them into threads.
 """
 
+from collections.abc import Sequence
 from datetime import datetime
-from typing import List, Optional, Sequence
+from typing import Optional
 
 from pyramid.security import Allow, DENY_ALL
 from sqlalchemy import (
@@ -91,7 +92,7 @@ class MessageConversation(DatabaseModel):
     # is dangerous and *will* break if user_id values ever get larger than integers
     # can hold. I'm comfortable doing something that will only be an issue if the site
     # reaches 2.1 billion users though, I think this would be the least of the problems.
-    unread_user_ids: List[int] = Column(
+    unread_user_ids: list[int] = Column(
         ARRAY(Integer), nullable=False, server_default="{}"
     )
 
diff --git a/tildes/tildes/models/model_query.py b/tildes/tildes/models/model_query.py
index 2203772..74cde0d 100644
--- a/tildes/tildes/models/model_query.py
+++ b/tildes/tildes/models/model_query.py
@@ -5,7 +5,8 @@
 # pylint: disable=self-cls-assignment
 
 from __future__ import annotations
-from typing import Any, Iterator, TypeVar
+from collections.abc import Iterator
+from typing import Any, TypeVar
 
 from pyramid.request import Request
 from sqlalchemy import event
diff --git a/tildes/tildes/models/pagination.py b/tildes/tildes/models/pagination.py
index 86312bd..9727502 100644
--- a/tildes/tildes/models/pagination.py
+++ b/tildes/tildes/models/pagination.py
@@ -4,8 +4,9 @@
 """Contains the PaginatedQuery and PaginatedResults classes."""
 
 from __future__ import annotations
+from collections.abc import Iterator, Sequence
 from itertools import chain
-from typing import Any, Iterator, List, Optional, Sequence, TypeVar
+from typing import Any, Optional, TypeVar
 
 from pyramid.request import Request
 from sqlalchemy import Column, func, inspect
@@ -39,12 +40,12 @@ class PaginatedQuery(ModelQuery):
         if not self.is_reversed:
             return super().__iter__()
 
-        results: List[ModelType] = list(super().__iter__())
+        results: list[ModelType] = list(super().__iter__())
 
         return iter(reversed(results))
 
     @property
-    def sorting_columns(self) -> List[Column]:
+    def sorting_columns(self) -> list[Column]:
         """Return the columns being used for sorting."""
         if not self._sort_column:
             raise AttributeError
@@ -56,7 +57,7 @@ class PaginatedQuery(ModelQuery):
             return [self._sort_column]
 
     @property
-    def sorting_columns_desc(self) -> List[Column]:
+    def sorting_columns_desc(self) -> list[Column]:
         """Return descending versions of the sorting columns."""
         return [col.desc() for col in self.sorting_columns]
 
diff --git a/tildes/tildes/models/topic/topic.py b/tildes/tildes/models/topic/topic.py
index 6e9435b..1c0fdd6 100644
--- a/tildes/tildes/models/topic/topic.py
+++ b/tildes/tildes/models/topic/topic.py
@@ -4,10 +4,11 @@
 """Contains the Topic class."""
 
 from __future__ import annotations
+from collections.abc import Iterable
 from datetime import datetime, timedelta
 from itertools import chain
 from pathlib import PurePosixPath
-from typing import Any, Dict, Iterable, List, Optional, TYPE_CHECKING
+from typing import Any, Optional, TYPE_CHECKING
 from urllib.parse import urlparse
 
 from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
@@ -122,7 +123,7 @@ class Topic(DatabaseModel):
     _markdown: Optional[str] = deferred(Column("markdown", Text))
     rendered_html: Optional[str] = Column(Text)
     link: Optional[str] = Column(Text)
-    content_metadata: Dict[str, Any] = Column(
+    content_metadata: dict[str, Any] = Column(
         MutableDict.as_mutable(JSONB(none_as_null=True))
     )
     num_comments: int = Column(Integer, nullable=False, server_default="0")
@@ -130,7 +131,7 @@ class Topic(DatabaseModel):
     _is_voting_closed: bool = Column(
         "is_voting_closed", Boolean, nullable=False, server_default="false", index=True
     )
-    tags: List[str] = Column(TagList, nullable=False, server_default="{}")
+    tags: list[str] = Column(TagList, nullable=False, server_default="{}")
     is_official: bool = Column(Boolean, nullable=False, server_default="false")
     is_locked: bool = Column(Boolean, nullable=False, server_default="false")
     search_tsv: Any = deferred(Column(TSVECTOR))
@@ -184,7 +185,7 @@ class Topic(DatabaseModel):
             self.last_edited_time = utc_now()
 
     @property
-    def important_tags(self) -> List[str]:
+    def important_tags(self) -> list[str]:
         """Return only the topic's "important" tags."""
         global_important_tags = ["nsfw", "spoiler"]
 
@@ -200,7 +201,7 @@ class Topic(DatabaseModel):
         ]
 
     @property
-    def unimportant_tags(self) -> List[str]:
+    def unimportant_tags(self) -> list[str]:
         """Return only the topic's tags that *aren't* considered "important"."""
         important_tags = set(self.important_tags)
         return [tag for tag in self.tags if tag not in important_tags]
@@ -552,7 +553,7 @@ class Topic(DatabaseModel):
         return self.content_metadata.get(key)
 
     @property
-    def content_metadata_for_display(self) -> List[str]:
+    def content_metadata_for_display(self) -> list[str]:
         """Return a list of the content's metadata strings, suitable for display."""
         if not self.content_type:
             return []
@@ -582,7 +583,7 @@ class Topic(DatabaseModel):
         return metadata_strings
 
     @property
-    def content_metadata_fields_for_display(self) -> Dict[str, str]:
+    def content_metadata_fields_for_display(self) -> dict[str, str]:
         """Return a dict of the metadata fields and values, suitable for display."""
         if not self.content_metadata:
             return {}
diff --git a/tildes/tildes/models/topic/topic_query.py b/tildes/tildes/models/topic/topic_query.py
index debf499..8eb63a8 100644
--- a/tildes/tildes/models/topic/topic_query.py
+++ b/tildes/tildes/models/topic/topic_query.py
@@ -4,7 +4,8 @@
 """Contains the TopicQuery class."""
 
 from __future__ import annotations
-from typing import Any, Sequence
+from collections.abc import Sequence
+from typing import Any
 
 from pyramid.request import Request
 from sqlalchemy import func
diff --git a/tildes/tildes/models/topic/topic_schedule.py b/tildes/tildes/models/topic/topic_schedule.py
index 763a59a..4a2f8f5 100644
--- a/tildes/tildes/models/topic/topic_schedule.py
+++ b/tildes/tildes/models/topic/topic_schedule.py
@@ -4,7 +4,7 @@
 """Contains the TopicSchedule class."""
 
 from datetime import datetime
-from typing import List, Optional
+from typing import Optional
 
 from dateutil.rrule import rrule
 from jinja2.sandbox import SandboxedEnvironment
@@ -58,7 +58,7 @@ class TopicSchedule(DatabaseModel):
         nullable=False,
     )
     markdown: str = Column(Text, nullable=False)
-    tags: List[str] = Column(TagList, nullable=False, server_default="{}")
+    tags: list[str] = Column(TagList, nullable=False, server_default="{}")
     next_post_time: Optional[datetime] = Column(
         TIMESTAMP(timezone=True), nullable=True, index=True
     )
@@ -79,7 +79,7 @@ class TopicSchedule(DatabaseModel):
         group: Group,
         title: str,
         markdown: str,
-        tags: List[str],
+        tags: list[str],
         next_post_time: datetime,
         recurrence_rule: Optional[rrule] = None,
         user: Optional[User] = None,
diff --git a/tildes/tildes/models/user/user.py b/tildes/tildes/models/user/user.py
index 86e6dac..933f4d7 100644
--- a/tildes/tildes/models/user/user.py
+++ b/tildes/tildes/models/user/user.py
@@ -4,7 +4,7 @@
 """Contains the User class."""
 
 from datetime import datetime, timedelta
-from typing import List, NoReturn, Optional
+from typing import NoReturn, Optional
 
 from pyotp import TOTP
 from pyramid.security import (
@@ -84,7 +84,7 @@ class User(DatabaseModel):
     )
     two_factor_enabled: bool = Column(Boolean, nullable=False, server_default="false")
     two_factor_secret: Optional[str] = deferred(Column(Text))
-    two_factor_backup_codes: List[str] = deferred(Column(ARRAY(Text)))
+    two_factor_backup_codes: list[str] = deferred(Column(ARRAY(Text)))
     created_time: datetime = Column(
         TIMESTAMP(timezone=True),
         nullable=False,
@@ -130,7 +130,7 @@ class User(DatabaseModel):
     ban_expiry_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
     home_default_order: Optional[TopicSortOption] = Column(ENUM(TopicSortOption))
     home_default_period: Optional[str] = Column(Text)
-    filtered_topic_tags: List[str] = Column(
+    filtered_topic_tags: list[str] = Column(
         TagList, nullable=False, server_default="{}"
     )
     comment_label_weight: Optional[float] = Column(REAL)
@@ -322,7 +322,7 @@ class User(DatabaseModel):
         return self.num_unread_messages + self.num_unread_notifications
 
     @property
-    def auth_principals(self) -> List[str]:
+    def auth_principals(self) -> list[str]:
         """Return the user's authorization principals (used for permissions)."""
         principals = [permission.auth_principal for permission in self.permissions]
 
diff --git a/tildes/tildes/request_methods.py b/tildes/tildes/request_methods.py
index 8fcfd44..f6e25ae 100644
--- a/tildes/tildes/request_methods.py
+++ b/tildes/tildes/request_methods.py
@@ -3,7 +3,7 @@
 
 """Define and attach request methods to the Pyramid request object."""
 
-from typing import Any, Dict, Optional, Tuple
+from typing import Any, Optional
 
 from pyramid.config import Configurator
 from pyramid.httpexceptions import HTTPTooManyRequests
@@ -114,7 +114,7 @@ def apply_rate_limit(request: Request, action_name: str) -> None:
 
 
 def current_listing_base_url(
-    request: Request, query: Optional[Dict[str, Any]] = None
+    request: Request, query: Optional[dict[str, Any]] = None
 ) -> str:
     """Return the "base" url for the current listing route.
 
@@ -123,7 +123,7 @@ def current_listing_base_url(
 
     The `query` argument allows adding query variables to the generated url.
     """
-    base_vars_by_route: Dict[str, Tuple[str, ...]] = {
+    base_vars_by_route: dict[str, tuple[str, ...]] = {
         "bookmarks": ("per_page", "type"),
         "group": ("order", "period", "per_page", "tag", "unfiltered"),
         "group_search": ("order", "period", "per_page", "q"),
@@ -149,7 +149,7 @@ def current_listing_base_url(
 
 
 def current_listing_normal_url(
-    request: Request, query: Optional[Dict[str, Any]] = None
+    request: Request, query: Optional[dict[str, Any]] = None
 ) -> str:
     """Return the "normal" url for the current listing route.
 
@@ -158,7 +158,7 @@ def current_listing_normal_url(
 
     The `query` argument allows adding query variables to the generated url.
     """
-    normal_vars_by_route: Dict[str, Tuple[str, ...]] = {
+    normal_vars_by_route: dict[str, tuple[str, ...]] = {
         "bookmarks": ("order", "period", "per_page"),
         "votes": ("order", "period", "per_page"),
         "group": ("order", "period", "per_page"),
diff --git a/tildes/tildes/schemas/fields.py b/tildes/tildes/schemas/fields.py
index 436fbb9..ad8770b 100644
--- a/tildes/tildes/schemas/fields.py
+++ b/tildes/tildes/schemas/fields.py
@@ -5,7 +5,8 @@
 
 import enum
 import re
-from typing import Any, Mapping, Optional, Type
+from collections.abc import Mapping
+from typing import Any, Optional
 
 import sqlalchemy_utils
 from marshmallow.exceptions import ValidationError
@@ -24,7 +25,9 @@ DataType = Optional[Mapping[str, Any]]
 class Enum(Field):
     """Field for a native Python Enum (or subclasses)."""
 
-    def __init__(self, enum_class: Optional[Type] = None, *args: Any, **kwargs: Any):
+    def __init__(
+        self, enum_class: Optional[type[enum.Enum]] = None, *args: Any, **kwargs: Any
+    ):
         """Initialize the field with an optional enum class."""
         # pylint: disable=keyword-arg-before-vararg
         super().__init__(*args, **kwargs)
diff --git a/tildes/tildes/schemas/topic.py b/tildes/tildes/schemas/topic.py
index 289b0f7..41e2aae 100644
--- a/tildes/tildes/schemas/topic.py
+++ b/tildes/tildes/schemas/topic.py
@@ -4,7 +4,6 @@
 """Validation/dumping schema for topics."""
 
 import re
-import typing
 from typing import Any
 from urllib.parse import urlparse
 
@@ -65,7 +64,7 @@ class TopicSchema(Schema):
 
         new_data = data.copy()
 
-        tags: typing.List[str] = []
+        tags: list[str] = []
 
         for tag in new_data["tags"]:
             tag = tag.lower()
@@ -99,7 +98,7 @@ class TopicSchema(Schema):
         return new_data
 
     @validates("tags")
-    def validate_tags(self, value: typing.List[str]) -> None:
+    def validate_tags(self, value: list[str]) -> None:
         """Validate the tags field, raising an error if an issue exists.
 
         Note that tags are validated by ensuring that each tag would be a valid group
diff --git a/tildes/tildes/scrapers/embedly_scraper.py b/tildes/tildes/scrapers/embedly_scraper.py
index de97de2..71e438b 100644
--- a/tildes/tildes/scrapers/embedly_scraper.py
+++ b/tildes/tildes/scrapers/embedly_scraper.py
@@ -3,7 +3,7 @@
 
 """Contains the EmbedlyScraper class."""
 
-from typing import Any, Dict
+from typing import Any
 from urllib.parse import urlparse
 
 import requests
@@ -35,7 +35,7 @@ class EmbedlyScraper:
 
     def scrape_url(self, url: str) -> ScraperResult:
         """Scrape a url and return the result."""
-        params: Dict[str, Any] = {"key": self.api_key, "format": "json", "url": url}
+        params: dict[str, Any] = {"key": self.api_key, "format": "json", "url": url}
 
         response = requests.get(
             "https://api.embedly.com/1/extract", params=params, timeout=5
@@ -45,7 +45,7 @@ class EmbedlyScraper:
         return ScraperResult(url, ScraperType.EMBEDLY, response.json())
 
     @staticmethod
-    def get_metadata_from_result(result: ScraperResult) -> Dict[str, Any]:
+    def get_metadata_from_result(result: ScraperResult) -> dict[str, Any]:
         """Get the metadata that we're interested in out of a scrape result."""
         if result.scraper_type != ScraperType.EMBEDLY:
             raise ValueError("Can't process a result from a different scraper.")
diff --git a/tildes/tildes/scrapers/youtube_scraper.py b/tildes/tildes/scrapers/youtube_scraper.py
index c6ac65b..89cc804 100644
--- a/tildes/tildes/scrapers/youtube_scraper.py
+++ b/tildes/tildes/scrapers/youtube_scraper.py
@@ -5,7 +5,7 @@
 
 import re
 from datetime import timedelta
-from typing import Any, Dict
+from typing import Any
 from urllib.parse import parse_qs, urlparse
 
 import requests
@@ -59,7 +59,7 @@ class YoutubeScraper:
         if not video_id:
             raise ValueError("Invalid url, no video ID found.")
 
-        params: Dict[str, Any] = {
+        params: dict[str, Any] = {
             "key": self.api_key,
             "id": video_id,
             "part": "snippet,contentDetails,statistics",
@@ -80,7 +80,7 @@ class YoutubeScraper:
         return ScraperResult(url, ScraperType.YOUTUBE, video_data)
 
     @classmethod
-    def get_metadata_from_result(cls, result: ScraperResult) -> Dict[str, Any]:
+    def get_metadata_from_result(cls, result: ScraperResult) -> dict[str, Any]:
         """Get the metadata that we're interested in out of a scrape result."""
         if result.scraper_type != ScraperType.YOUTUBE:
             raise ValueError("Can't process a result from a different scraper.")
diff --git a/tildes/tildes/tweens.py b/tildes/tildes/tweens.py
index b378534..9031c1f 100644
--- a/tildes/tildes/tweens.py
+++ b/tildes/tildes/tweens.py
@@ -3,8 +3,8 @@
 
 """Contains Pyramid "tweens", used to insert additional logic into request-handling."""
 
+from collections.abc import Callable
 from time import time
-from typing import Callable
 
 from prometheus_client import Histogram
 from pyramid.config import Configurator
diff --git a/tildes/tildes/typing.py b/tildes/tildes/typing.py
index 3991e41..1818355 100644
--- a/tildes/tildes/typing.py
+++ b/tildes/tildes/typing.py
@@ -3,8 +3,8 @@
 
 """Custom type aliases to use in type annotations."""
 
-from typing import Any, List, Tuple
+from typing import Any
 
 # types for an ACE (Access Control Entry), and the ACL (Access Control List) of them
-AceType = Tuple[str, Any, str]
-AclType = List[AceType]
+AceType = tuple[str, Any, str]
+AclType = list[AceType]
diff --git a/tildes/tildes/views/api/web/exceptions.py b/tildes/tildes/views/api/web/exceptions.py
index 40b4ac1..827ba61 100644
--- a/tildes/tildes/views/api/web/exceptions.py
+++ b/tildes/tildes/views/api/web/exceptions.py
@@ -3,7 +3,7 @@
 
 """Web API exception views."""
 
-from typing import Sequence
+from collections.abc import Sequence
 from urllib.parse import quote, urlparse, urlunparse
 
 from marshmallow.exceptions import ValidationError
diff --git a/tildes/tildes/views/bookmarks.py b/tildes/tildes/views/bookmarks.py
index 728cfe0..4c64fcc 100644
--- a/tildes/tildes/views/bookmarks.py
+++ b/tildes/tildes/views/bookmarks.py
@@ -1,6 +1,6 @@
 """Views relating to bookmarks."""
 
-from typing import Optional, Type, Union
+from typing import Optional, Union
 
 from pyramid.request import Request
 from pyramid.view import view_config
@@ -27,7 +27,7 @@ def get_bookmarks(
     # pylint: disable=unused-argument
     user = request.user
 
-    bookmark_cls: Union[Type[CommentBookmark], Type[TopicBookmark]]
+    bookmark_cls: Union[type[CommentBookmark], type[TopicBookmark]]
 
     if post_type == "comment":
         post_cls = Comment
diff --git a/tildes/tildes/views/decorators.py b/tildes/tildes/views/decorators.py
index a697e71..2c41123 100644
--- a/tildes/tildes/views/decorators.py
+++ b/tildes/tildes/views/decorators.py
@@ -3,7 +3,8 @@
 
 """Contains decorators for view functions."""
 
-from typing import Any, Callable, Dict, Union
+from collections.abc import Callable
+from typing import Any, Union
 
 from marshmallow import EXCLUDE
 from marshmallow.fields import Field
@@ -15,7 +16,7 @@ from webargs import dict2schema, pyramidparser
 
 
 def use_kwargs(
-    argmap: Union[Schema, Dict[str, Field]], location: str = "query", **kwargs: Any
+    argmap: Union[Schema, dict[str, Field]], location: str = "query", **kwargs: Any
 ) -> Callable:
     """Wrap the webargs @use_kwargs decorator with preferred default modifications.
 
diff --git a/tildes/tildes/views/exceptions.py b/tildes/tildes/views/exceptions.py
index 8b424b7..f119f28 100644
--- a/tildes/tildes/views/exceptions.py
+++ b/tildes/tildes/views/exceptions.py
@@ -3,7 +3,7 @@
 
 """Views used by Pyramid when an exception is raised."""
 
-from typing import Sequence
+from collections.abc import Sequence
 from urllib.parse import quote_plus
 
 from marshmallow import ValidationError
diff --git a/tildes/tildes/views/financials.py b/tildes/tildes/views/financials.py
index 0ee33c4..3b50bfd 100644
--- a/tildes/tildes/views/financials.py
+++ b/tildes/tildes/views/financials.py
@@ -5,7 +5,7 @@
 
 from collections import defaultdict
 from decimal import Decimal
-from typing import Dict, List, Optional
+from typing import Optional
 
 from pyramid.request import Request
 from pyramid.view import view_config
@@ -30,7 +30,7 @@ def get_financials(request: Request) -> dict:
     )
 
     # split the entries up by type
-    entries: Dict[str, List] = defaultdict(list)
+    entries = defaultdict(list)
     for entry in financial_entries:
         entries[entry.entry_type.name.lower()].append(entry)
 
@@ -43,7 +43,7 @@ def get_financials(request: Request) -> dict:
     }
 
 
-def get_financial_data(db_session: Session) -> Optional[Dict[str, Decimal]]:
+def get_financial_data(db_session: Session) -> Optional[dict[str, Decimal]]:
     """Return financial data used to render the donation goal box."""
     # get the total sum for each entry type in the financials table relevant to today
     financial_totals = (
diff --git a/tildes/tildes/views/user.py b/tildes/tildes/views/user.py
index 2bc8162..71d4695 100644
--- a/tildes/tildes/views/user.py
+++ b/tildes/tildes/views/user.py
@@ -3,7 +3,7 @@
 
 """Views related to a specific user."""
 
-from typing import List, Optional, Type, Union
+from typing import Optional, Union
 
 from enum import Enum
 from marshmallow.fields import String
@@ -50,8 +50,8 @@ def get_user(
         anchor_type = None
         per_page = 20
 
-    types_to_query: List[Union[Type[Topic], Type[Comment]]]
-    order_options: Optional[Union[Type[TopicSortOption], Type[CommentSortOption]]]
+    types_to_query: list[Union[type[Topic], type[Comment]]]
+    order_options: Optional[Union[type[TopicSortOption], type[CommentSortOption]]]
 
     if post_type == "topic":
         types_to_query = [Topic]
@@ -111,8 +111,8 @@ def get_user_search(
     """Generate the search results page for a user's posts."""
     user = request.context
 
-    types_to_query: List[Union[Type[Topic], Type[Comment]]]
-    order_options: Union[Type[TopicSortOption], Type[CommentSortOption]]
+    types_to_query: list[Union[type[Topic], type[Comment]]]
+    order_options: Union[type[TopicSortOption], type[CommentSortOption]]
 
     if post_type == "topic":
         types_to_query = [Topic]
@@ -170,7 +170,7 @@ def get_invite(request: Request) -> dict:
 def _get_user_posts(
     request: Request,
     user: User,
-    types_to_query: List[Union[Type[Topic], Type[Comment]]],
+    types_to_query: list[Union[type[Topic], type[Comment]]],
     anchor_type: Optional[str],
     before: Optional[str],
     after: Optional[str],
diff --git a/tildes/tildes/views/votes.py b/tildes/tildes/views/votes.py
index 76a25f3..7f812ec 100644
--- a/tildes/tildes/views/votes.py
+++ b/tildes/tildes/views/votes.py
@@ -1,6 +1,6 @@
 """Views relating to voted posts."""
 
-from typing import Optional, Type, Union
+from typing import Optional, Union
 
 from pyramid.request import Request
 from pyramid.view import view_config
@@ -27,7 +27,7 @@ def get_voted_posts(
     # pylint: disable=unused-argument
     user = request.user
 
-    vote_cls: Union[Type[CommentVote], Type[TopicVote]]
+    vote_cls: Union[type[CommentVote], type[TopicVote]]
 
     if post_type == "comment":
         post_cls = Comment