Repository: rackspace/runway (language: python)

Issue:
Upgrade Pydantic to v2
Runway uses Pydantic extensively, and awhile back they moved to v2. We haven't received those updates due to package pinning and some changes will be required to move to v2.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index a20c1f1a1..a79a0a930 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -87,6 +87,8 @@ jobs:
       (needs.changes.outputs.infra-test == 'true' || needs.changes.outputs.infra-test-alt == 'true')
     runs-on: ubuntu-latest
     steps:
+      - name: ⤵️ Check out code from GitHub
+        uses: actions/checkout@v4
       - name: 🏗 Setup Python
         uses: finleyfamily/action-setup-python@v1.0.0
         with:
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index c71af0350..c003f2319 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -2,7 +2,6 @@
     "recommendations": [
         "DavidAnson.vscode-markdownlint",
         "editorconfig.editorconfig",
-        "littlefoxteam.vscode-python-test-adapter",
         "ms-python.python",
         "ms-python.vscode-pylance"
     ]
diff --git a/Makefile b/Makefile
index dec475f1e..6301b7de8 100644
--- a/Makefile
+++ b/Makefile
@@ -74,7 +74,7 @@ lint-black: ## run black
 
 lint-pyright: ## run pyright
 	@echo "Running pyright..."
-	@npm exec --no -- pyright --venv-path ./
+	@npm exec --no -- pyright --venvpath ./
 	@echo ""
 
 lint-ruff: ## run ruff
diff --git a/docs/source/cdk/configuration.rst b/docs/source/cdk/configuration.rst
index 5f4801cec..b243dc154 100644
--- a/docs/source/cdk/configuration.rst
+++ b/docs/source/cdk/configuration.rst
@@ -82,7 +82,7 @@ Options
 .. _cdk.build_steps:
 
 .. data:: build_steps
-  :type: Optional[List[str]]
+  :type: list[str] | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/configuration.rst b/docs/source/cfngin/configuration.rst
index 5adafefbf..bccfe99f1 100644
--- a/docs/source/cfngin/configuration.rst
+++ b/docs/source/cfngin/configuration.rst
@@ -106,7 +106,7 @@ Top-Level Fields
   .. _cfngin-bucket:
 
   .. attribute:: cfngin_bucket
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     By default, CloudFormation templates are pushed into an S3 bucket and CloudFormation is pointed to the template in that bucket when launching or updating stacks.
@@ -158,7 +158,7 @@ Top-Level Fields
       The format of the default value is now ``cfngin-${namespace}-${region}``.
 
   .. attribute:: cfngin_bucket_region
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     AWS Region where :attr:`~cfngin.config.cfngin_bucket` is located.
@@ -172,7 +172,7 @@ Top-Level Fields
         cfngin_bucket_region: us-east-1
 
   .. attribute:: cfngin_cache_dir
-    :type: Optional[str]
+    :type: str | None
     :value: ./.runway/
 
     Path to a local directory that CFNgin will use for local caching.
@@ -187,7 +187,7 @@ Top-Level Fields
         cfngin_cache_dir: ./.runway
 
   .. attribute:: log_formats
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str]
     :value: {}
 
     Customize log message formatting by log level.
@@ -202,7 +202,7 @@ Top-Level Fields
         debug: "[%(asctime)s] %(levelname)s %(threadName)s %(name)s:%(lineno)d(%(funcName)s): %(message)s"
 
   .. attribute:: lookups
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str]
     :value: {}
 
     Lookups allow you to create custom methods which take a value and are resolved at runtime time.
@@ -224,7 +224,7 @@ Top-Level Fields
       conf_value: ${custom query}
 
   .. attribute:: mappings
-    :type: Optional[Dict[str, Dict[str, Dict[str, Any]]]]
+    :type: dict[str, dict[str, dict[str, Any]]]
     :value: {}
 
     Mappings are dictionaries that are provided as `Mappings <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html>`__ to each CloudFormation stack that CFNgin produces.
@@ -264,7 +264,7 @@ Top-Level Fields
       namespace: ${namespace}-${environment}
 
   .. attribute:: namespace_delimiter
-    :type: Optional[str]
+    :type: str | None
     :value: "-"
 
     By default, ``-`` will be used as a delimiter between the :attr:`~cfngin.config.namespace` and the declared stack name to deploy the actual CloudFormation stack name that gets created.
@@ -279,7 +279,7 @@ Top-Level Fields
       namespace_delimiter: ""
 
   .. attribute:: package_sources
-    :type: Optional[cfngin.package_sources]
+    :type: cfngin.package_sources
     :value: {}
 
     See :ref:`Remote Sources <cfngin_remote_sources>` for detailed information.
@@ -296,7 +296,7 @@ Top-Level Fields
           ...
 
   .. attribute:: persistent_graph_key
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     Used to track the *state* of stacks defined in configuration file.
@@ -310,7 +310,7 @@ Top-Level Fields
       persistent_graph_key: unique-key.json
 
   .. attribute:: post_deploy
-    :type: Optional[List[cfngin.hook]]
+    :type: list[cfngin.hook]
     :value: []
 
     Python functions/methods that are executed after processing the stacks in the config while using the :ref:`commands:deploy` command.
@@ -330,7 +330,7 @@ Top-Level Fields
       The CFNgin bucket is now created using a CloudFormation stack.
 
   .. attribute:: post_destroy
-    :type: Optional[List[cfngin.hook]]
+    :type: list[cfngin.hook]
     :value: []
 
     Python functions/methods that are executed after processing the stacks in the config while using the :ref:`commands:destroy` command.
@@ -344,7 +344,7 @@ Top-Level Fields
         - path: do.something
 
   .. attribute:: pre_deploy
-    :type: Optional[List[cfngin.hook]]
+    :type: list[cfngin.hook]
     :value: []
 
     Python functions/methods that are executed before processing the stacks in the config while using the :ref:`commands:deploy` command.
@@ -361,7 +361,7 @@ Top-Level Fields
       *pre_build* renamed to *pre_deploy*.
 
   .. attribute:: pre_destroy
-    :type: Optional[List[cfngin.hook]]
+    :type: list[cfngin.hook]
     :value: []
 
     Python functions/methods that are executed before processing the stacks in the config while using the :ref:`commands:destroy` command.
@@ -375,7 +375,7 @@ Top-Level Fields
         - path: do.something
 
   .. attribute:: service_role
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     By default CFNgin doesn't specify a service role when executing changes to CloudFormation stacks.
@@ -391,7 +391,7 @@ Top-Level Fields
       service_role: arn:aws:iam::123456789012:role/name
 
   .. attribute:: stacks
-    :type: Optional[List[cfngin.stack]]
+    :type: list[cfngin.stack]
     :Value: []
 
     This is the core part of the config where the CloudFormations stacks that will be deployed in the environment are defined.
@@ -399,7 +399,7 @@ Top-Level Fields
     See Stack_ for more information.
 
   .. attribute:: sys_path
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     A path to be added to ``$PATH`` while processing the configuration file.
@@ -413,7 +413,7 @@ Top-Level Fields
       sys_path: ./  # most common value to use
 
   .. attribute:: tags
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str]
     :value: {"cfngin_namespace": namespace}
 
     A dictionary of tags to add to all stacks.
@@ -438,7 +438,7 @@ Top-Level Fields
       tags: {}
 
   .. attribute:: template_indent
-    :type: Optional[int]
+    :type: int | None
     :value: 4
 
     Number of spaces per indentation level to use when rendering/outputting CloudFormation templates.
@@ -486,7 +486,7 @@ Stack
           CidrBlock: 10.128.0.0/16
 
   .. attribute:: class_path
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     A python importable path to the |Blueprint| class to be used.
@@ -501,7 +501,7 @@ Stack
           class_path: example.BlueprintClass
 
   .. attribute:: description
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     A short description to apply to the stack.
@@ -516,7 +516,7 @@ Stack
           description: An Example Stack
 
   .. attribute:: enabled
-    :type: Optional[bool]
+    :type: bool
     :value: True
 
     Whether to deploy/update the stack.
@@ -534,7 +534,7 @@ Stack
           enabled: ${enable_another_stack}
 
   .. attribute:: in_progress_behavior
-    :type: Optional[Literal["wait"]]
+    :type: Literal["wait"] | None
     :value: None
 
     Specifies the behavior for when a stack is in ``CREATE_IN_PROGRESS`` or ``UPDATE_IN_PROGRESS``.
@@ -550,7 +550,7 @@ Stack
           in_progress_behavior: wait
 
   .. attribute:: locked
-    :type: Optional[bool]
+    :type: bool
     :value: False
 
     Whether the stack should be updated after initial deployment.
@@ -582,7 +582,7 @@ Stack
         - name: example-stack
 
   .. attribute:: protected
-    :type: Optional[bool]
+    :type: bool
     :value: False
 
     Whether to force all updates to be performed interactively.
@@ -599,7 +599,7 @@ Stack
           protected: ${protected_another_stack}
 
   .. attribute:: required_by
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     A list of other stacks that require this stack.
@@ -618,7 +618,7 @@ Stack
           ...
 
   .. attribute:: requires
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     A list of other stacks that this stack requires.
@@ -637,7 +637,7 @@ Stack
           ...
 
   .. attribute:: stack_name
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     The name used when creating the CloudFormation stack.
@@ -653,7 +653,7 @@ Stack
           stack_name: another-name
 
   .. attribute:: stack_policy_path
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     Path to a JSON formatted stack policy that will be applied when the CloudFormation stack is created and/or updated.
@@ -668,7 +668,7 @@ Stack
           stack_policy_path: ./stack_policies/example-stack.json
 
   .. attribute:: tags
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str]
     :value: {}
 
     A dictionary of tags to add to the Stack.
@@ -688,7 +688,7 @@ Stack
             example: value
 
   .. attribute:: template_path
-    :type: Optional[str]
+    :type: str | None
 
     Path to a raw CloudFormation template (JSON or YAML).
     Can be relative to the working directory (e.g. templates stored alongside the configuration file), or relative to a directory in the *$PATH* (i.e. for loading templates retrieved via :attr:`~cfngin.config.package_sources`).
@@ -705,7 +705,7 @@ Stack
           template_path: remote/path/templates/another-stack.json
 
   .. attribute:: termination_protection
-    :type: Optional[bool]
+    :type: bool
     :value: False
 
     Whether the stack will be protected from termination by CloudFormation.
@@ -725,7 +725,7 @@ Stack
           termination_protection: ${termination_protection_another_stack}
 
   .. attribute:: timeout
-    :type: Optional[int]
+    :type: int | None
     :value: None
 
     Specifies the amount of time, in minutes, that CloudFormation should allot before timing out stack creation operations.
@@ -743,7 +743,7 @@ Stack
           timeout: 120
 
   .. attribute:: variables
-    :type: Optional[Dict[str, Any]]
+    :type: dict[str, Any]
     :value: {}
 
     A dictionary of Variables_ to pass to the |Blueprint| when rendering the CloudFormation template.
diff --git a/docs/source/cfngin/hooks/acm.Certificate.rst b/docs/source/cfngin/hooks/acm.Certificate.rst
index 5d4b3262c..67cc463a6 100644
--- a/docs/source/cfngin/hooks/acm.Certificate.rst
+++ b/docs/source/cfngin/hooks/acm.Certificate.rst
@@ -38,7 +38,7 @@ Args
 ****
 
 .. data:: alt_names
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: []
   :noindex:
 
@@ -62,7 +62,7 @@ Args
   This must exist in the same account that the certificate will be created in.
 
 .. data:: stack_name
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -71,7 +71,7 @@ Args
   If the is provided in a deploy stage, its needs to be provided in the matching destroy stage.
 
 .. data:: ttl
-  :type: Optional[int]
+  :type: int | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/aws_lambda.upload_lambda_functions.rst b/docs/source/cfngin/hooks/aws_lambda.upload_lambda_functions.rst
index c7aa99958..08835a245 100644
--- a/docs/source/cfngin/hooks/aws_lambda.upload_lambda_functions.rst
+++ b/docs/source/cfngin/hooks/aws_lambda.upload_lambda_functions.rst
@@ -37,14 +37,14 @@ Args
 ****
 
 .. data:: bucket
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
   Custom bucket to upload functions to. If not provided, |cfngin_bucket| will be used.
 
 .. data:: bucket_region
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -52,14 +52,14 @@ Args
   If not provided, :attr:`~cfngin.config.cfngin_bucket_region` will be used.
 
 .. data:: prefix
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
   S3 key prefix to prepend to the uploaded zip name.
 
 .. data:: follow_symlinks
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -73,7 +73,7 @@ Args
   The canned S3 object ACL to be applied to the uploaded payload.
 
 .. data:: functions
-  :type: Dict[str, Any]
+  :type: dict[str, Any]
   :noindex:
 
   Configurations of desired payloads to build.
@@ -81,7 +81,7 @@ Args
   Each value should itself be a dictionary, with the following data:
 
   .. data:: docker_file
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -89,7 +89,7 @@ Args
     Must provide exactly one of ``docker_file``, ``docker_image``, or ``runtime``.
 
   .. data:: docker_image
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -97,7 +97,7 @@ Args
     Must provide exactly one of ``docker_file``, ``docker_image``, or ``runtime``.
 
   .. data:: dockerize_pip
-    :type: Optional[Union[bool, Literal["non-linux"]]]
+    :type: bool | Literal["non-linux"] | None
     :value: None
     :noindex:
 
@@ -106,7 +106,7 @@ Args
     To use this option Docker must be installed.
 
   .. data:: exclude
-    :type: Optional[Union[List[str], str]]
+    :type: list[str] | str
     :value: None
     :noindex:
 
@@ -116,7 +116,7 @@ Args
     Commonly ignored files are already excluded by default, such as ``.git``, ``.svn``, ``__pycache__``, ``*.pyc``, ``.gitignore``, etc.
 
   .. data:: include
-    :type: Optional[List[str], str]
+    :type: list[str] | str | None
     :value: None
     :noindex:
 
@@ -139,7 +139,7 @@ Args
     So, for example, all the files contained directly under this directory will be added to the root of the ZIP file.
 
   .. data:: python_path
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -147,7 +147,7 @@ Args
     If not provided, the current python interpreter will be used for ``pip`` and ``pipenv`` will be used from the current ``$PATH``.
 
   .. data:: runtime
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -156,7 +156,7 @@ Args
     Must provide exactly one of ``docker_file``, ``docker_image``, or ``runtime``.
 
   .. data:: use_pipenv
-    :type: Optional[bool]
+    :type: bool | None
     :value: False
     :noindex:
 
diff --git a/docs/source/cfngin/hooks/command.run_command.rst b/docs/source/cfngin/hooks/command.run_command.rst
index 1f90bc2f5..9f66d75c4 100644
--- a/docs/source/cfngin/hooks/command.run_command.rst
+++ b/docs/source/cfngin/hooks/command.run_command.rst
@@ -14,20 +14,20 @@ Args
 ****
 
 .. data:: command
-  :type: Union[List[str], str]
+  :type: list[str] | str
   :noindex:
 
   Command(s) to run.
 
 .. data:: capture
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
   If enabled, capture the command's stdout and stderr, and return them in the hook result.
 
 .. data:: interactive
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -35,14 +35,14 @@ Args
   Otherwise, stdin will be set to the null device.
 
 .. data:: ignore_status
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
   Don't fail the hook if the command returns a non-zero status.
 
 .. data:: quiet
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -50,7 +50,7 @@ Args
   Should not be enabled if ``capture`` is also enabled.
 
 .. data:: stdin
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -58,7 +58,7 @@ Args
   Implicitly disables ``interactive``.
 
 .. data:: env
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str] | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/docker.image.build.rst b/docs/source/cfngin/hooks/docker.image.build.rst
index bde66f7de..2bb3dd4e5 100644
--- a/docs/source/cfngin/hooks/docker.image.build.rst
+++ b/docs/source/cfngin/hooks/docker.image.build.rst
@@ -21,14 +21,14 @@ Args
 ****
 
 .. data:: docker
-  :type: Optional[Dict[str, Any]]
+  :type: dict[str, Any]
   :value: {}
   :noindex:
 
   Options for ``docker image build``.
 
   .. data:: buildargs
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str] | None
     :value: None
     :noindex:
 
@@ -42,7 +42,7 @@ Args
     Optional if providing a path to a zip file.
 
   .. data:: extra_hosts
-    :type: Optional[Dict[str, str]]
+    :type: dict[str, str] | None
     :value: None
     :noindex:
 
@@ -57,14 +57,14 @@ Args
     Always remove intermediate containers, even after unsuccessful builds.
 
   .. data:: isolation
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
     Isolation technology used during build.
 
   .. data:: network_mode
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -78,7 +78,7 @@ Args
     Don't use cache when set to ``True``.
 
   .. data:: platform
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -107,21 +107,21 @@ Args
     Squash the resulting image layers into a single layer.
 
   .. data:: tag
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
     Optional name and tag to apply to the base image when it is built.
 
   .. data:: target
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
     Name of the build-stage to build in a multi-stage Dockerfile.
 
   .. data:: timeout
-    :type: Optional[int]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -135,14 +135,14 @@ Args
     If ``True`` and if the docker client configuration file (``~/.docker/config.json`` by default) contains a proxy configuration, the corresponding environment variables will be set in the container being built.
 
 .. data:: dockerfile
-  :type: Optional[str]
+  :type: str | None
   :value: "./Dockerfile"
   :noindex:
 
   Path within the build context to the Dockerfile.
 
 .. data:: ecr_repo
-  :type: Optional[Dict[str, Optional[str]]]
+  :type: dict[str, str | None] | None
   :value: None
   :noindex:
 
@@ -153,7 +153,7 @@ Args
   If using a public registry, ``repo_name`` and ``registry_alias``.
 
   .. data:: account_id
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -161,7 +161,7 @@ Args
     it will be acquired automatically if needed.
 
   .. data:: aws_region
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -169,7 +169,7 @@ Args
     automatically if needed.
 
   .. data:: registry_alias
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -182,13 +182,13 @@ Args
     The name of the repository.
 
 .. data:: path
-  :type: Optional[str]
+  :type: str | None
   :noindex:
 
   Path to the directory containing the Dockerfile.
 
 .. data:: repo
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -196,7 +196,7 @@ Args
   If providing one of the other repo values, leave this value empty.
 
 .. data:: tags
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: ["latest"]
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/docker.image.push.rst b/docs/source/cfngin/hooks/docker.image.push.rst
index 56c9c8ffe..256456a0d 100644
--- a/docs/source/cfngin/hooks/docker.image.push.rst
+++ b/docs/source/cfngin/hooks/docker.image.push.rst
@@ -19,7 +19,7 @@ Args
 ****
 
 .. data:: ecr_repo
-  :type: Optional[Dict[str, Optional[str]]]
+  :type: dict[str, str | None] | None
   :value: None
   :noindex:
 
@@ -30,7 +30,7 @@ Args
   If using a public registry, ``repo_name`` and ``registry_alias``.
 
   .. data:: account_id
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -38,7 +38,7 @@ Args
     it will be acquired automatically if needed.
 
   .. data:: aws_region
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -46,7 +46,7 @@ Args
     automatically if needed.
 
   .. data:: registry_alias
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -59,7 +59,7 @@ Args
     The name of the repository.
 
 .. data:: image
-  :type: Optional[DockerImage]
+  :type: DockerImage | None
   :value: None
   :noindex:
 
@@ -70,7 +70,7 @@ Args
   If providing a value for this field, do not provide a value for ``ecr_repo`` or ``repo``.
 
 .. data:: repo
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -78,7 +78,7 @@ Args
   If providing one of the other repo values or ``image``, leave this value empty.
 
 .. data:: tags
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: ["latest"]
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/docker.image.remove.rst b/docs/source/cfngin/hooks/docker.image.remove.rst
index ab3637ed7..1a94b0ed3 100644
--- a/docs/source/cfngin/hooks/docker.image.remove.rst
+++ b/docs/source/cfngin/hooks/docker.image.remove.rst
@@ -18,7 +18,7 @@ Args
 ****
 
 .. data:: ecr_repo
-  :type: Optional[Dict[str, Optional[str]]]
+  :type: dict[str, str | None] | None
   :value: None
   :noindex:
 
@@ -29,7 +29,7 @@ Args
   If using a public registry, ``repo_name`` and ``registry_alias``.
 
   .. data:: account_id
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -37,7 +37,7 @@ Args
     it will be acquired automatically if needed.
 
   .. data:: aws_region
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -45,7 +45,7 @@ Args
     automatically if needed.
 
   .. data:: registry_alias
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -58,14 +58,14 @@ Args
     The name of the repository.
 
 .. data:: force
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
   Whether to force the removal of the image.
 
 .. data:: image
-  :type: Optional[DockerImage]
+  :type: DockerImage | None
   :value: None
   :noindex:
 
@@ -76,14 +76,14 @@ Args
   If providing a value for this field, do not provide a value for ``ecr_repo`` or ``repo``.
 
 .. data:: noprune
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
   Whether to delete untagged parents.
 
 .. data:: repo
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -91,7 +91,7 @@ Args
   If providing one of the other repo values or ``image``, leave this value empty.
 
 .. data:: tags
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: ["latest"]
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/docker.login.rst b/docs/source/cfngin/hooks/docker.login.rst
index d7d34f403..bf306856a 100644
--- a/docs/source/cfngin/hooks/docker.login.rst
+++ b/docs/source/cfngin/hooks/docker.login.rst
@@ -20,14 +20,14 @@ Args
 ****
 
 .. data:: dockercfg_path
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
   Use a custom path for the Docker config file (``$HOME/.docker/config.json`` if present, otherwise ``$HOME/.dockercfg``).
 
 .. data:: ecr
-  :type: Optional[Dict[str, Optional[str]]]
+  :type: dict[str, str | None] | None
   :value: None
   :noindex:
 
@@ -38,7 +38,7 @@ Args
   If using a public registry, ``repo_name`` and ``registry_alias``.
 
   .. data:: account_id
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -46,14 +46,14 @@ Args
     it will be acquired automatically if needed.
 
   .. data:: alias
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
     If it is a public registry, provide the alias.
 
   .. data:: aws_region
-    :type: Optional[str]
+    :type: str | None
     :value: None
     :noindex:
 
@@ -61,7 +61,7 @@ Args
     automatically if needed.
 
 .. data:: email
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -74,7 +74,7 @@ Args
   The plaintext password for the registry account.
 
 .. data:: registry
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -83,7 +83,7 @@ Args
   If providing a value for this field, do not provide a value for ``ecr``.
 
 .. data:: username
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/iam.create_ecs_service_role.rst b/docs/source/cfngin/hooks/iam.create_ecs_service_role.rst
index 28958e118..bf5be6f7c 100644
--- a/docs/source/cfngin/hooks/iam.create_ecs_service_role.rst
+++ b/docs/source/cfngin/hooks/iam.create_ecs_service_role.rst
@@ -17,7 +17,7 @@ Args
 ****
 
 .. data:: role_name
-  :type: Optional[str]
+  :type: str | None
   :value: "ecsServiceRole"
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/index.rst b/docs/source/cfngin/hooks/index.rst
index e7f51e0f5..0463388e3 100644
--- a/docs/source/cfngin/hooks/index.rst
+++ b/docs/source/cfngin/hooks/index.rst
@@ -24,7 +24,7 @@ Only the following actions allow pre/post hooks:
   - :attr:`~cfngin.hook.args`
 
   .. attribute:: args
-    :type: Optional[Dict[str, Any]]
+    :type: dict[str, Any]
     :value: {}
 
     A dictionary of arguments to pass to the hook.
@@ -42,7 +42,7 @@ Only the following actions allow pre/post hooks:
             key: ${val}
 
   .. attribute:: data_key
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     If set, and the hook returns data (a dictionary or ``pydantic.BaseModel``), the results will be stored in :attr:`CfnginContext.hook_data <runway.context.CfnginContext.hook_data>` with the ``data_key`` as its key.
@@ -54,7 +54,7 @@ Only the following actions allow pre/post hooks:
         - data_key: example-key
 
   .. attribute:: enabled
-    :type: Optional[bool]
+    :type: bool
     :value: True
 
     Whether to execute the hook every CFNgin run.
@@ -78,7 +78,7 @@ Only the following actions allow pre/post hooks:
         - path: runway.cfngin.hooks.command.run_command
 
   .. attribute:: required
-    :type: Optional[bool]
+    :type: bool
     :value: True
 
     Whether to stop execution if the hook fails.
@@ -153,12 +153,11 @@ Example Hook Function
   :caption: local_path/hooks/my_hook.py
 
   """My hook."""
-  from typing import Dict, Optional
 
 
   def do_something(
       *, is_failure: bool = True, name: str = "Kevin", **_kwargs: str
-  ) -> Optional[Dict[str, str]]:
+  ) -> dict[str, str] | None:
       """Do something."""
       if is_failure:
           return None
@@ -191,7 +190,7 @@ These can then be used to parse the values provided in the :attr:`~cfngin.hook.a
 
   """My hook."""
   import logging
-  from typing import TYPE_CHECKING, Any, Dict, Optional
+  from typing import TYPE_CHECKING, Any
 
   from runway.utils import BaseModel
   from runway.cfngin.hooks.protocols import CfnginHookProtocol
@@ -219,11 +218,11 @@ These can then be used to parse the values provided in the :attr:`~cfngin.hook.a
       """My class does a thing.
 
       Keyword Args:
-          is_failure (bool): Force the hook to fail if true.
-          name (str): Name used in the response.
+          is_failure: Force the hook to fail if true.
+          name: Name used in the response.
 
       Returns:
-          Dict[str, str]: Response message is stored in ``result``.
+          Response message is stored in ``result``.
 
       Example:
       .. code-block:: yaml
@@ -252,7 +251,7 @@ These can then be used to parse the values provided in the :attr:`~cfngin.hook.a
           self.args.tags.update(context.tags)
           self.context = context
 
-      def post_deploy(self) -> Optional[Dict[str, str]]:
+      def post_deploy(self) -> dict[str, str] | None:
           """Run during the **post_deploy** stage."""
           if self.args["is_failure"]:
               return None
diff --git a/docs/source/cfngin/hooks/keypair.ensure_keypair_exists.rst b/docs/source/cfngin/hooks/keypair.ensure_keypair_exists.rst
index 6b1e7e887..bcba69651 100644
--- a/docs/source/cfngin/hooks/keypair.ensure_keypair_exists.rst
+++ b/docs/source/cfngin/hooks/keypair.ensure_keypair_exists.rst
@@ -20,7 +20,7 @@ Args
   Name of the key pair to create
 
 .. data:: public_key_path
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -28,7 +28,7 @@ Args
   Incompatible with the SSM options, as the private key will not be available for storing.
 
 .. data:: ssm_key_id
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -36,7 +36,7 @@ Args
   parameter with. If omitted, the default key will be used.
 
 .. data:: ssm_parameter_name
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/hooks/ssm.parameter.SecureString.rst b/docs/source/cfngin/hooks/ssm.parameter.SecureString.rst
index 9e471a538..dc3aa9a31 100644
--- a/docs/source/cfngin/hooks/ssm.parameter.SecureString.rst
+++ b/docs/source/cfngin/hooks/ssm.parameter.SecureString.rst
@@ -24,14 +24,14 @@ Args
 ****
 
 .. data:: allowed_pattern
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
   A regular expression used to validate the parameter value.
 
 .. data:: data_type
-  :type: Optional[Literal["aws:ec2:image", "text"]]
+  :type: Literal["aws:ec2:image", "text"] | None
   :value: None
   :noindex:
 
@@ -39,7 +39,7 @@ Args
   Supported data types include plain text and Amazon Machine Image IDs.
 
 .. data:: description
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -54,7 +54,7 @@ Args
   Can be used alongside **overwrite** to always update a parameter.
 
 .. data:: key_id
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -76,7 +76,7 @@ Args
   If this is set to ``False`` and the parameter already exists, the parameter will not be updated and a warning will be logged.
 
 .. data:: policies
-  :type: Optional[Union[List[Dict[str, Any]], str]]
+  :type: list[dict[str, Any]] | str | None
   :value: None
   :noindex:
 
@@ -84,7 +84,7 @@ Args
   This field takes a JSON array.
 
 .. data:: tags
-  :type: Optional[Union[Dict[str, str], List[TagTypeDef]]]
+  :type: dict[str, str] | list[TagTypeDef] | None
   :value: None
   :noindex:
 
@@ -98,7 +98,7 @@ Args
   The parameter tier to assign to a parameter.
 
 .. data:: value
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
diff --git a/docs/source/cfngin/lookups/ami.rst b/docs/source/cfngin/lookups/ami.rst
index 953dc560c..00b240df9 100644
--- a/docs/source/cfngin/lookups/ami.rst
+++ b/docs/source/cfngin/lookups/ami.rst
@@ -14,13 +14,13 @@ Any other arguments specified but not listed below are sent as filters to the AW
 For example, ``architecture:x86_64`` would add a filter.
 
 .. data:: region
-  :type: Optional[str]
+  :type: str | None
   :noindex:
 
   AWS region to search (e.g. ``us-east-1``). Defaults to the current region.
 
 .. data:: owners
-  :type: Union[List[str], str]
+  :type: List[str] | str | None
   :noindex:
 
   At least one owner must be specified in the query (e.g. ``amazon``, ``self``, or an AWS account ID).
@@ -33,7 +33,7 @@ For example, ``architecture:x86_64`` would add a filter.
   Regex pattern for the name of the AMI (e.g. ``my-ubuntu-server-[0-9]+``).
 
 .. data:: executable_users
-  :type: Optional[str]
+  :type: str | None
   :noindex:
 
   ``amazon``, ``self``, or an AWS account ID.
diff --git a/docs/source/cfngin/lookups/index.rst b/docs/source/cfngin/lookups/index.rst
index 1023d4963..3243a150d 100644
--- a/docs/source/cfngin/lookups/index.rst
+++ b/docs/source/cfngin/lookups/index.rst
@@ -109,9 +109,10 @@ If using boto3 in a lookup, use :meth:`context.get_session() <runway.context.Cfn
 .. code-block:: python
 
   """Example lookup."""
+
   from __future__ import annotations
 
-  from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Union
+  from typing import TYPE_CHECKING, Any, ClassVar
 
   from runway.cfngin.utils import read_value_from_path
   from runway.lookups.handlers.base import LookupHandler
@@ -124,16 +125,16 @@ If using boto3 in a lookup, use :meth:`context.get_session() <runway.context.Cfn
   class MylookupLookup(LookupHandler):
       """My lookup."""
 
-      TYPE_NAME: Final[Literal["mylookup"]] = "mylookup"
+      TYPE_NAME: ClassVar[str] = "my_lookup"
       """Name that the Lookup is registered as."""
 
       @classmethod
       def handle(
           cls,
           value: str,
-          context: Union[CfnginContext, RunwayContext],
+          context: CfnginContext | RunwayContext,
           *_args: Any,
-          provider: Optional[Provider] = None,
+          provider: Provider | None = None,
           **_kwargs: Any
       ) -> str:
           """Do something.
diff --git a/docs/source/cfngin/remote_sources.rst b/docs/source/cfngin/remote_sources.rst
index 071ffb293..e61ae9b83 100644
--- a/docs/source/cfngin/remote_sources.rst
+++ b/docs/source/cfngin/remote_sources.rst
@@ -14,7 +14,7 @@ By defining these additional sources you are able to extend your *$PATH* to make
   There are three different types of package sources - git repository, local, and AWS S3.
 
   .. attribute:: git
-    :type: Optional[List[cfngin.package_source.git]]
+    :type: list[cfngin.package_source.git]
     :value: []
 
     A list of git repositories to include when processing the configuration file.
@@ -29,7 +29,7 @@ By defining these additional sources you are able to extend your *$PATH* to make
           ...
 
   .. attribute:: local
-    :type: Optional[List[cfngin.package_source.local]]
+    :type: list[cfngin.package_source.local]
     :value: []
 
     A list of additional local directories to include when processing the configuration file.
@@ -44,7 +44,7 @@ By defining these additional sources you are able to extend your *$PATH* to make
           ...
 
   .. attribute:: s3
-    :type: Optional[List[cfngin.package_source.s3]]
+    :type: list[cfngin.package_source.s3]
     :value: []
 
     A list of AWS S3 objects to include when processing the configuration file.
@@ -72,7 +72,7 @@ Git Repository
   The cache location is defined by :attr:`cfngin.config.cfngin_cache_dir`.
 
   .. attribute:: branch
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     Name of a branch to checkout after cloning the git repository.
@@ -87,7 +87,7 @@ Git Repository
           - branch: master
 
   .. attribute:: commit
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     After cloning the git repository, reset *HEAD* to the given commit hash.
@@ -102,7 +102,7 @@ Git Repository
           - commit: 5d83f7ff1ad6527233be2c27e9f68816599b6c57
 
   .. attribute:: configs
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     Configuration files from this source location can also be used by specifying a list of file paths.
@@ -120,7 +120,7 @@ Git Repository
             - example-02.yml
 
   .. attribute:: paths
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     A list of subdirectories within the source location that should be added to *$PATH*.
@@ -135,7 +135,7 @@ Git Repository
             - another/
 
   .. attribute:: tag
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     After cloning the git repository, reset *HEAD* to the given tag.
@@ -172,7 +172,7 @@ Local
   Package source located on a local disk.
 
   .. attribute:: configs
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     Configuration files from this source location can also be used by specifying a list of file paths.
@@ -190,7 +190,7 @@ Local
             - example-02.yml
 
   .. attribute:: paths
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     A list of subdirectories within the source location that should be added to *$PATH*.
@@ -242,7 +242,7 @@ AWS S3
           - bucket: example-bucket
 
   .. attribute:: configs
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     Configuration files from this source location can also be used by specifying a list of file paths.
@@ -273,7 +273,7 @@ AWS S3
             - key: path/to/example.zip
 
   .. attribute:: paths
-    :type: Optional[List[str]]
+    :type: list[str]
     :value: []
 
     A list of subdirectories within the source location that should be added to *$PATH*.
@@ -288,7 +288,7 @@ AWS S3
             - another/
 
   .. attribute:: requester_pays
-    :type: Optional[bool]
+    :type: bool
     :value: False
 
     Confirms that the requester knows that they will be charged for the request
@@ -301,7 +301,7 @@ AWS S3
           - requester_pays: true
 
   .. attribute:: use_latest
-    :type: Optional[bool]
+    :type: bool
     :value: True
 
     Update the local copy if the last modified date in AWS S3 changes.
diff --git a/docs/source/kubernetes/configuration.rst b/docs/source/kubernetes/configuration.rst
index 25cf6087e..4418e8f45 100644
--- a/docs/source/kubernetes/configuration.rst
+++ b/docs/source/kubernetes/configuration.rst
@@ -13,7 +13,7 @@ Options
 *******
 
 .. data:: kubectl_version
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -27,7 +27,7 @@ Options
       kubectl_version: 1.14.5
 
 .. data:: overlay_path
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
diff --git a/docs/source/runway_config.rst b/docs/source/runway_config.rst
index d5972e7b9..dc73838f2 100644
--- a/docs/source/runway_config.rst
+++ b/docs/source/runway_config.rst
@@ -13,7 +13,7 @@ Top-Level Configuration
 ***********************
 
 .. attribute:: deployments
-  :type: List[deployment]
+  :type: list[deployment]
 
   A list of deployments that will be processed in the order they are defined.
   See Deployment_ for detailed information about defining this value.
@@ -71,7 +71,7 @@ Top-Level Configuration
   .. versionadded:: 1.11.0
 
 .. attribute:: tests
-  :type: Optional[List[test]]
+  :type: list[test] | None
   :value: []
 
   List of Runway test definitions that are executed with the :ref:`test command <commands:test>`.
@@ -90,7 +90,7 @@ Top-Level Configuration
 .. _runway-variables:
 
 .. attribute:: variables
-  :type: Optional[Dict[str, Any]]
+  :type: dict[str, Any] | None
   :value: {}
 
   Runway variables are used to fill values that could change based on any number of circumstances.
@@ -130,7 +130,7 @@ Top-Level Configuration
   .. versionadded 1.4.0
 
   .. data:: variables.file_path
-    :type: Optional[str]
+    :type: str | None
 
     Explicit path to a variables file that will be loaded and merged with the variables defined here.
 
@@ -141,7 +141,7 @@ Top-Level Configuration
         file_path: some-file.yml
 
   .. data:: variables.sys_path
-    :type: Optional[str]
+    :type: str | None
     :value: ./
 
     Directory to use as the root of a relative :data:`variables.file_path`.
@@ -199,7 +199,7 @@ Deployment
 
 
   .. attribute:: account_alias
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     An `AWS account alias <https://docs.aws.amazon.com/IAM/latest/UserGuide/console_account-alias.html>`__ use to verify the currently assumed role or credentials.
@@ -228,7 +228,7 @@ Deployment
       No longer accepts a :class:`typing.Dict`.
 
   .. attribute:: account_id
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     An AWS account ID use to verify the currently assumed role or credentials.
@@ -257,7 +257,7 @@ Deployment
       No longer accepts a :class:`typing.Dict`.
 
   .. attribute:: assume_role
-    :type: Optional[assume_role_definition, str]
+    :type: assume_role_definition | str | None
     :value: {}
 
     Assume an AWS IAM role when processing the deployment.
@@ -312,7 +312,7 @@ Deployment
         An identifier for the assumed role session.
 
   .. attribute:: env_vars
-    :type: Optional[Dict[str, Union[List[str], str]]]
+    :type: dict[str, list[str] | str] | None
     :value: {}
 
     Additional variables to add to the environment when processing the deployment.
@@ -347,7 +347,7 @@ Deployment
       The entire value of the field is used for all environments.
 
   .. attribute:: environments
-    :type: Optional[Dict[str, Union[bool, List[str], str]]]
+    :type: dict[str, bool | list[str] | str] | None
     :value: {}
 
     Explicitly enable/disable the deployment for a specific deploy environment, AWS Account ID, and AWS Region combination.
@@ -386,7 +386,7 @@ Deployment
       If defined and the current deploy environment is missing from the definition, processing will be skipped.
 
   .. attribute:: modules
-    :type: List[Union[module, str]]
+    :type: list[module | str]
 
     A list of modules to process as part of a deployment.
 
@@ -399,7 +399,7 @@ Deployment
             - path: sampleapp-02.cfn
 
   .. attribute:: module_options
-    :type: Optional[Union[Dict[str, Any], str]]
+    :type: dict[str, Any] | str | None
     :value: {}
 
     Options that are passed directly to the modules within this deployment.
@@ -434,7 +434,7 @@ Deployment
           example: value
 
   .. attribute:: name
-    :type: Optional[str]
+    :type: str | None
     :value: None
 
     The name of the deployment to be displayed in logs and the interactive selection menu.
@@ -446,7 +446,7 @@ Deployment
         - name: networking
 
   .. attribute:: parallel_regions
-    :type: Optional[Union[List[str], str]]
+    :type: list[str] | str | None
     :value: []
 
     A list of AWS Regions to process asynchronously.
@@ -488,7 +488,7 @@ Deployment
     .. versionadded:: 1.3.0
 
   .. attribute:: parameters
-    :type: Optional[Union[Dict[str, Any], str]]
+    :type: dict[str, Any] | str | None
     :value: {}
 
     Used to pass variable values to modules in place of an environment configuration file.
@@ -517,7 +517,7 @@ Deployment
     .. versionadded:: 1.4.0
 
   .. attribute:: regions
-    :type: Optional[Union[Dict[str, Union[List[str], str], List[str], str]]
+    :type: dict[str, list[str] | str] | list[str] | str | None
     :value: []
 
     A list of AWS Regions to process this deployment in.
@@ -618,7 +618,7 @@ Module
   - :attr:`~module.path`
 
   .. attribute:: class_path
-    :type: Optional[str]
+    :type: str | None
     :value: null
 
     .. note::
@@ -636,7 +636,7 @@ Module
           - class_path: runway.module.cloudformation.CloudFormation
 
   .. attribute:: env_vars
-    :type: Optional[Dict[str, Union[List[str], str]]]
+    :type: dict[str, list[str] | str] | None
     :value: {}
 
     Additional variables to add to the environment when processing the deployment.
@@ -674,7 +674,7 @@ Module
       The entire value of the field is used for all environments.
 
   .. attribute:: environments
-    :type: Optional[Dict[str, Union[bool, List[str], str]]]
+    :type: dict[str, bool | list[str] | str] | None
     :value: {}
 
     Explicitly enable/disable the deployment for a specific deploy environment, AWS Account ID, and AWS Region combination.
@@ -716,7 +716,7 @@ Module
       If defined and the current deploy environment is missing from the definition, processing will be skipped.
 
   .. attribute:: name
-    :type: Optional[str]
+    :type: str | None
 
     The name of the module to be displayed in logs and the interactive selection menu.
 
@@ -730,7 +730,7 @@ Module
           - name: networking
 
   .. attribute:: options
-    :type: Optional[Union[Dict[str, Any], str]]
+    :type: dict[str, Any] | str | None
     :value: {}
 
     Options that are passed directly to the module type handler class.
@@ -772,7 +772,7 @@ Module
           example: value
 
   .. attribute:: parallel
-    :type: Optional[List[module]]
+    :type: list[module] | None
     :value: []
 
     List of `module` definitions that can be executed asynchronously.
@@ -792,7 +792,7 @@ Module
             - path: sampleapp-02.cfn
 
   .. attribute:: parameters
-    :type: Optional[Union[Dict[str, Any], str]]
+    :type: dict[str, Any] | str | None
     :value: {}
 
     Used to pass variable values to modules in place of an environment configuration file.
@@ -824,7 +824,7 @@ Module
     .. versionadded:: 1.4.0
 
   .. attribute:: path
-    :type: Optional[Union[str, Path]]
+    :type: str | Path | None
 
     Directory (relative to the Runway config file) containing IaC.
     The directory can either be on the local file system or a network accessible location.
@@ -842,7 +842,7 @@ Module
     .. versionadded:: 1.4.0
 
   .. attribute:: tags
-    :type: Optional[List[str]]
+    :type: list[str] | None
     :value: []
 
     A list of strings to categorize the module which can be used with the CLI to quickly select a group of modules.
@@ -859,7 +859,7 @@ Module
             - type:network
 
   .. attribute:: type
-    :type: Optional[str]
+    :type: str | None
 
     Explicitly define the type of IaC contained within the directory.
     This can be useful when Runway fails to automatically determine the correct module type.
@@ -998,7 +998,7 @@ Test
   - :attr:`test.required`
 
   .. attribute:: args
-    :type: Optional[Union[Dict[str, Any], str]]
+    :type: dict[str, Any] | str | None
     :value: {}
 
     Arguments to be passed to the test.
@@ -1014,7 +1014,7 @@ Test
               - echo "Hello world"
 
   .. attribute:: name
-    :type: Optional[str]
+    :type: str | None
 
     Name of the test.
     Used to more easily identify where different tests begin/end in the logs and to identify which tests failed.
diff --git a/docs/source/serverless/configuration.rst b/docs/source/serverless/configuration.rst
index c7e851fe3..f7acae16b 100644
--- a/docs/source/serverless/configuration.rst
+++ b/docs/source/serverless/configuration.rst
@@ -25,7 +25,7 @@ Options
 Options specific to Serverless Framework modules.
 
 .. data:: args
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: []
   :noindex:
 
@@ -43,7 +43,7 @@ Options specific to Serverless Framework modules.
   .. versionadded:: 1.4.0
 
 .. data:: extend_serverless_yml
-  :type: Optional[Dict[str, Any]]
+  :type: dict[str, Any]
   :value: {}
   :noindex:
 
@@ -62,7 +62,7 @@ Options specific to Serverless Framework modules.
   .. versionadded:: 1.8.0
 
 .. data:: promotezip
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str]
   :value: {}
   :noindex:
 
diff --git a/docs/source/staticsite/advanced_features.rst b/docs/source/staticsite/advanced_features.rst
index 0125264d4..1e06591d1 100644
--- a/docs/source/staticsite/advanced_features.rst
+++ b/docs/source/staticsite/advanced_features.rst
@@ -92,7 +92,7 @@ See :ref:`Static Site Examples <staticsite-examples>` to see how to do this in A
   The destination name of the file to create.
 
 .. data:: file
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -101,7 +101,7 @@ See :ref:`Static Site Examples <staticsite-examples>` to see how to do this in A
   This or ``content`` must be specified.
 
 .. data:: content_type
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -113,7 +113,7 @@ See :ref:`Static Site Examples <staticsite-examples>` to see how to do this in A
   * ``text/yaml`` to serialize ``content`` into YAML.
 
 .. data:: content
-  :type: Optional[Union[str, List[Any], Dict[str, Any]]]
+  :type: str, list[Any], dict[str, Any] | None
   :value: None
   :noindex:
 
diff --git a/docs/source/staticsite/configuration.rst b/docs/source/staticsite/configuration.rst
index 27ce0cdb4..ee6fb6236 100644
--- a/docs/source/staticsite/configuration.rst
+++ b/docs/source/staticsite/configuration.rst
@@ -15,7 +15,7 @@ Options
 *******
 
 .. data:: build_output
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -28,7 +28,7 @@ Options
       build_output: dist
 
 .. data:: build_steps
-  :type: Optional[List[str]]
+  :type: list[str]
   :value: []
   :noindex:
 
@@ -43,7 +43,7 @@ Options
         - npm run build
 
 .. data:: extra_files
-  :type: Optional[List[Dict[str, Union[str, Dict[str, Any]]]]]
+  :type: list[dict[str, str | dict[str, Any]]]
   :value: []
   :noindex:
 
@@ -84,7 +84,7 @@ Options
   .. versionadded:: 1.9.0
 
 .. data:: pre_build_steps
-  :type: Optional[List[Dict[str, str]]]
+  :type: list[dict[str, str]]
   :value: []
   :noindex:
 
@@ -101,7 +101,7 @@ Options
           cwd: ../myothermodule
 
 .. data:: source_hashing
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str]
   :value: {}
   :noindex:
 
@@ -128,7 +128,7 @@ Parameters
 **********
 
 .. data:: cloudformation_service_role
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -158,7 +158,7 @@ Parameters
 .. _staticsite_acmcert_arn:
 
 .. data:: staticsite_acmcert_arn
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -172,7 +172,7 @@ Parameters
       staticsite_acmcert_arn: arn:aws:acm:<region>:<account-id>:certificate/<cert>
 
 .. data:: staticsite_aliases
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -190,7 +190,7 @@ Parameters
 .. _staticsite_auth_at_edge:
 
 .. data:: staticsite_auth_at_edge
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -208,7 +208,7 @@ Parameters
 .. _staticsite_cf_disable:
 
 .. data:: staticsite_cf_disable
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -226,7 +226,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_compress
-  :type: Optional[bool]
+  :type: bool
   :value: True
   :noindex:
 
@@ -239,7 +239,7 @@ Parameters
       staticsite_compress: false
 
 .. data:: staticsite_cookie_settings
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str] | None
   :value: {"idToken": "Path=/; Secure; SameSite=Lax", "accessToken": "Path=/; Secure; SameSite=Lax", "refreshToken": "Path=/; Secure; SameSite=Lax", "nonce": "Path=/; Secure; HttpOnly; Max-Age=1800; SameSite=Lax"}
   :noindex:
 
@@ -262,7 +262,7 @@ Parameters
 .. _staticsite_create_user_pool:
 
 .. data:: staticsite_create_user_pool
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -281,7 +281,7 @@ Parameters
 .. _staticsite_custom_error_responses:
 
 .. data:: staticsite_custom_error_responses
-  :type: Optional[List[Dict[str, Union[int, str]]]]
+  :type: list[dict[str, int | str]]
   :value: []
   :noindex:
 
@@ -297,7 +297,7 @@ Parameters
           ResponsePagePath: /index.html
 
 .. data:: staticsite_enable_cf_logging
-  :type: Optional[bool]
+  :type: bool
   :value: True
   :noindex:
 
@@ -310,7 +310,7 @@ Parameters
       staticsite_enable_cf_logging: true
 
 .. data:: staticsite_http_headers
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str] | None
   :value: {"Content-Security-Policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; font-src 'self' 'unsafe-inline' 'unsafe-eval' data: https:; object-src 'none'; connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com", "Strict-Transport-Security": "max-age=31536000;  includeSubdomains; preload", "Referrer-Policy": "same-origin", "X-XSS-Protection": "1; mode=block", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff"}
   :noindex:
 
@@ -337,7 +337,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_lambda_function_associations
-  :type: Optional[List[Dict[str, str]]]
+  :type: list[dict[str, str]]
   :value: []
   :noindex:
 
@@ -353,7 +353,7 @@ Parameters
           arn: arn:aws:lambda:<region>:<account-id>:function:<function>:<version>
 
 .. data:: staticsite_non_spa
-  :type: Optional[bool]
+  :type: bool
   :value: False
   :noindex:
 
@@ -374,7 +374,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_oauth_scopes
-  :type: Optional[List[str]]
+  :type: list[str] | None
   :value: ["phone", "email", "profile", "openid", "aws.cognito.signin.user.admin"]
   :noindex:
 
@@ -398,7 +398,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_redirect_path_auth_refresh
-  :type: Optional[str]
+  :type: str | None
   :value: "/refreshauth"
   :noindex:
 
@@ -415,7 +415,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_redirect_path_sign_in
-  :type: Optional[str]
+  :type: str | None
   :value: "/parseauth"
   :noindex:
 
@@ -433,7 +433,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_redirect_path_sign_out
-  :type: Optional[str]
+  :type: str | None
   :value: "/"
   :noindex:
 
@@ -453,7 +453,7 @@ Parameters
 .. _staticsite_rewrite_directory_index:
 
 .. data:: staticsite_rewrite_directory_index
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -466,7 +466,7 @@ Parameters
       staticsite_rewrite_directory_index: index.html
 
 .. data:: staticsite_role_boundary_arn
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -482,7 +482,7 @@ Parameters
   .. versionadded:: 1.8.0
 
 .. data:: staticsite_sign_out_url
-  :type: Optional[str]
+  :type: str | None
   :value: "/signout"
   :noindex:
 
@@ -499,7 +499,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_supported_identity_providers
-  :type: Optional[str]
+  :type: str | None
   :value: "COGNITO"
   :noindex:
 
@@ -516,7 +516,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_user_pool_arn
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -533,7 +533,7 @@ Parameters
   .. versionadded:: 1.5.0
 
 .. data:: staticsite_additional_redirect_domains
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -552,7 +552,7 @@ Parameters
   .. versionadded:: 1.14.0
 
 .. data:: staticsite_web_acl
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -565,7 +565,7 @@ Parameters
       staticsite_web_acl: arn:aws:waf::<account-id>:certificate/<cert>
 
 .. data:: staticsite_required_group
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
diff --git a/docs/source/terraform/configuration.rst b/docs/source/terraform/configuration.rst
index 78ad20f27..8e7225e7d 100644
--- a/docs/source/terraform/configuration.rst
+++ b/docs/source/terraform/configuration.rst
@@ -13,7 +13,7 @@ Options
 Options specific to Terraform Modules.
 
 .. data:: args
-  :type: Optional[Union[Dict[str, List[str]], List[str]]]
+  :type: dict[str, list[str]] | list[str] | None
   :value: None
   :noindex:
 
@@ -30,7 +30,7 @@ Options specific to Terraform Modules.
   .. versionadded:: 1.8.1
 
 .. data:: terraform_backend_config
-  :type: Optional[Dict[str, str]]
+  :type: dict[str, str] | None
   :value: {}
   :noindex:
 
@@ -50,7 +50,7 @@ Options specific to Terraform Modules.
     Added support for any *key: value*.
 
 .. data:: terraform_version
-  :type: Optional[str]
+  :type: str | None
   :value: None
   :noindex:
 
@@ -64,7 +64,7 @@ Options specific to Terraform Modules.
       terraform_version: 0.11.13
 
 .. data:: terraform_write_auto_tfvars
-  :type: Optional[bool]
+  :type: str | None
   :value: False
   :noindex:
 
diff --git a/infrastructure/blueprints/admin_role.py b/infrastructure/blueprints/admin_role.py
index caf91209d..69f7d3fc4 100644
--- a/infrastructure/blueprints/admin_role.py
+++ b/infrastructure/blueprints/admin_role.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, ClassVar, Optional
+from typing import TYPE_CHECKING, ClassVar
 
 import awacs.sts
 from awacs.aws import Allow, AWSPrincipal, PolicyDocument, Statement
@@ -45,7 +45,7 @@ def namespace(self) -> str:
         return self.context.namespace
 
     @cached_property
-    def role_name(self) -> Optional[str]:
+    def role_name(self) -> str | None:
         """Name of the role being created."""
         val = self.variables["RoleName"]
         if val == "":
diff --git a/infrastructure/blueprints/prevent_privilege_escalation.py b/infrastructure/blueprints/prevent_privilege_escalation.py
index b806a2158..8ba913db2 100644
--- a/infrastructure/blueprints/prevent_privilege_escalation.py
+++ b/infrastructure/blueprints/prevent_privilege_escalation.py
@@ -6,7 +6,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, ClassVar, Union
+from typing import TYPE_CHECKING, ClassVar
 
 import awacs.iam
 import awacs.sts
@@ -66,9 +66,9 @@ def approved_boundary_policies(self) -> list[Sub]:
         ]
 
     @cached_property
-    def deny_assume_role_not_resources(self) -> list[Union[str, Sub]]:
+    def deny_assume_role_not_resources(self) -> list[str | Sub]:
         """List of IAM Role ARNs that can be assumed."""
-        tmp: list[Union[str, Sub]] = [
+        tmp: list[str | Sub] = [
             Sub(f"arn:${{AWS::Partition}}:iam::${{AWS::AccountId}}:role/{self.namespace}-*")
         ]
         tmp.extend(self.variables["DenyAssumeRoleNotResources"])
diff --git a/package-lock.json b/package-lock.json
index 44b9923f0..62fe8f1df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
       "version": "0.0.0",
       "devDependencies": {
         "cspell": "^8.13.3",
-        "pyright": "^1.1.223"
+        "pyright": "^1.1.376"
       }
     },
     "node_modules/@cspell/cspell-bundled-dicts": {
@@ -919,6 +919,21 @@
       "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "dev": true
     },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
     "node_modules/gensequence": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz",
@@ -1124,16 +1139,20 @@
       }
     },
     "node_modules/pyright": {
-      "version": "1.1.223",
-      "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.223.tgz",
-      "integrity": "sha512-EGLKMQRmSkKNe/eDy6MEkqq5lF91C5MCMYxvmCgKGUjZHIa+HByJIiRY2cOTm7g2CAv0eE7Ift701VxLM5Q0iQ==",
+      "version": "1.1.376",
+      "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.376.tgz",
+      "integrity": "sha512-on/vUGr8aOF7L9tRdMn4Xzj3Zs+fAyR17MJ9XO3VjlsexutPcDcpWUBLvCZf3wYL0r+CmrAxKAxcBF9ktarHow==",
       "dev": true,
+      "license": "MIT",
       "bin": {
         "pyright": "index.js",
         "pyright-langserver": "langserver.index.js"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
       }
     },
     "node_modules/queue-microtask": {
@@ -2045,6 +2064,13 @@
       "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "dev": true
     },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "optional": true
+    },
     "gensequence": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz",
@@ -2188,10 +2214,13 @@
       "dev": true
     },
     "pyright": {
-      "version": "1.1.223",
-      "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.223.tgz",
-      "integrity": "sha512-EGLKMQRmSkKNe/eDy6MEkqq5lF91C5MCMYxvmCgKGUjZHIa+HByJIiRY2cOTm7g2CAv0eE7Ift701VxLM5Q0iQ==",
-      "dev": true
+      "version": "1.1.376",
+      "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.376.tgz",
+      "integrity": "sha512-on/vUGr8aOF7L9tRdMn4Xzj3Zs+fAyR17MJ9XO3VjlsexutPcDcpWUBLvCZf3wYL0r+CmrAxKAxcBF9ktarHow==",
+      "dev": true,
+      "requires": {
+        "fsevents": "~2.3.3"
+      }
     },
     "queue-microtask": {
       "version": "1.2.3",
diff --git a/package.json b/package.json
index be956a38d..15877b862 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "devDependencies": {
     "cspell": "^8.13.3",
-    "pyright": "^1.1.223"
+    "pyright": "^1.1.376"
   },
   "name": "runway",
   "version": "0.0.0"
diff --git a/poetry.lock b/poetry.lock
index 3d0a23011..21f52f6be 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -11,6 +11,17 @@ files = [
     {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
 ]
 
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
 [[package]]
 name = "attrs"
 version = "24.2.0"
@@ -1117,6 +1128,20 @@ files = [
     {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
 ]
 
+[[package]]
+name = "eval-type-backport"
+version = "0.2.0"
+description = "Like `typing._eval_type`, but lets older Python versions use newer typing features."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"},
+    {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
 [[package]]
 name = "exceptiongroup"
 version = "1.1.1"
@@ -2069,55 +2094,123 @@ files = [
 
 [[package]]
 name = "pydantic"
-version = "1.10.6"
-description = "Data validation and settings management using python type hints"
+version = "2.8.2"
+description = "Data validation using Python type hints"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+    {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
+    {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.20.1"
+typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.20.1"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
 files = [
-    {file = "pydantic-1.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9289065611c48147c1dd1fd344e9d57ab45f1d99b0fb26c51f1cf72cd9bcd31"},
-    {file = "pydantic-1.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c32b6bba301490d9bb2bf5f631907803135e8085b6aa3e5fe5a770d46dd0160"},
-    {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd9b9e98068fa1068edfc9eabde70a7132017bdd4f362f8b4fd0abed79c33083"},
-    {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c84583b9df62522829cbc46e2b22e0ec11445625b5acd70c5681ce09c9b11c4"},
-    {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b41822064585fea56d0116aa431fbd5137ce69dfe837b599e310034171996084"},
-    {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61f1f08adfaa9cc02e0cbc94f478140385cbd52d5b3c5a657c2fceb15de8d1fb"},
-    {file = "pydantic-1.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:32937835e525d92c98a1512218db4eed9ddc8f4ee2a78382d77f54341972c0e7"},
-    {file = "pydantic-1.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd5c531b22928e63d0cb1868dee76123456e1de2f1cb45879e9e7a3f3f1779b"},
-    {file = "pydantic-1.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e277bd18339177daa62a294256869bbe84df1fb592be2716ec62627bb8d7c81d"},
-    {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f15277d720aa57e173954d237628a8d304896364b9de745dcb722f584812c7"},
-    {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b243b564cea2576725e77aeeda54e3e0229a168bc587d536cd69941e6797543d"},
-    {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3ce13a558b484c9ae48a6a7c184b1ba0e5588c5525482681db418268e5f86186"},
-    {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ac1cd4deed871dfe0c5f63721e29debf03e2deefa41b3ed5eb5f5df287c7b70"},
-    {file = "pydantic-1.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:b1eb6610330a1dfba9ce142ada792f26bbef1255b75f538196a39e9e90388bf4"},
-    {file = "pydantic-1.10.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4ca83739c1263a044ec8b79df4eefc34bbac87191f0a513d00dd47d46e307a65"},
-    {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4e2a7cb409951988e79a469f609bba998a576e6d7b9791ae5d1e0619e1c0f2"},
-    {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53de12b4608290992a943801d7756f18a37b7aee284b9ffa794ee8ea8153f8e2"},
-    {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:60184e80aac3b56933c71c48d6181e630b0fbc61ae455a63322a66a23c14731a"},
-    {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:415a3f719ce518e95a92effc7ee30118a25c3d032455d13e121e3840985f2efd"},
-    {file = "pydantic-1.10.6-cp37-cp37m-win_amd64.whl", hash = "sha256:72cb30894a34d3a7ab6d959b45a70abac8a2a93b6480fc5a7bfbd9c935bdc4fb"},
-    {file = "pydantic-1.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3091d2eaeda25391405e36c2fc2ed102b48bac4b384d42b2267310abae350ca6"},
-    {file = "pydantic-1.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:751f008cd2afe812a781fd6aa2fb66c620ca2e1a13b6a2152b1ad51553cb4b77"},
-    {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12e837fd320dd30bd625be1b101e3b62edc096a49835392dcf418f1a5ac2b832"},
-    {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d92831d0115874d766b1f5fddcdde0c5b6c60f8c6111a394078ec227fca6d"},
-    {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:476f6674303ae7965730a382a8e8d7fae18b8004b7b69a56c3d8fa93968aa21c"},
-    {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a2be0a0f32c83265fd71a45027201e1278beaa82ea88ea5b345eea6afa9ac7f"},
-    {file = "pydantic-1.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:0abd9c60eee6201b853b6c4be104edfba4f8f6c5f3623f8e1dba90634d63eb35"},
-    {file = "pydantic-1.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6195ca908045054dd2d57eb9c39a5fe86409968b8040de8c2240186da0769da7"},
-    {file = "pydantic-1.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43cdeca8d30de9a897440e3fb8866f827c4c31f6c73838e3a01a14b03b067b1d"},
-    {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c19eb5163167489cb1e0161ae9220dadd4fc609a42649e7e84a8fa8fff7a80f"},
-    {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62"},
-    {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:528dcf7ec49fb5a84bf6fe346c1cc3c55b0e7603c2123881996ca3ad79db5bfc"},
-    {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:163e79386c3547c49366e959d01e37fc30252285a70619ffc1b10ede4758250a"},
-    {file = "pydantic-1.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:189318051c3d57821f7233ecc94708767dd67687a614a4e8f92b4a020d4ffd06"},
-    {file = "pydantic-1.10.6-py3-none-any.whl", hash = "sha256:acc6783751ac9c9bc4680379edd6d286468a1dc8d7d9906cd6f1186ed682b2b0"},
-    {file = "pydantic-1.10.6.tar.gz", hash = "sha256:cf95adb0d1671fc38d8c43dd921ad5814a735e7d9b4d9e437c088002863854fd"},
-]
-
-[package.dependencies]
-typing-extensions = ">=4.2.0"
-
-[package.extras]
-dotenv = ["python-dotenv (>=0.10.4)"]
-email = ["email-validator (>=1.0.3)"]
+    {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
+    {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
+    {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
+    {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
+    {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
+    {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
+    {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
+    {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
+    {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
+    {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
+    {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
+    {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
+    {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
+    {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 
 [[package]]
 name = "pygments"
@@ -3358,4 +3451,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9, <3.13"
-content-hash = "0d6a1510ca2fda7cd14106d6615720e3ff824668ad67f400338306a9da53c9df"
+content-hash = "e86930f251e333f695b226a3bb3150a2d3fe23d0243dfd8875ad02e18220e32e"
diff --git a/pyproject.toml b/pyproject.toml
index 9b9d41b72..92297d14e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ cfn_flip = "^1.2"  # only used in runway._cli.commands._gen_sample.utils
 click = "^8.0"
 coloredlogs = "*, !=15.0.1"
 docker = ">=3.0.0"  # used in runway.cfngin.hooks
+eval-type-backport = {python = "< 3.10", version = "*"}  # TODO (kyle): remove when dropping support for python 3.9
 formic2 = "*"  # only used in runway.cfngin.hooks.aws_lambda
 gitpython = "*"
 igittigitt = ">=2.0.5"
@@ -46,7 +47,7 @@ jinja2 = ">=2.7"  # used in runway.cfngin.blueprints.raw
 packaging = "*"  # component of setuptools needed for version compare
 pipenv = "2022.1.8"
 pyOpenSSL = "*"  # For embedded hook & associated script usage
-pydantic = "^1.4"
+pydantic = "^2.8.0"
 pyhcl = "^0.4"  # does not support HCL2, possibly move to extras_require in the future
 python-hcl2 = ">=3.0.0"
 pyyaml = ">5.4"
@@ -199,14 +200,13 @@ pythonPlatform = "All"
 pythonVersion = "3.9"
 reportDuplicateImport = "none"
 reportImportCycles = "none"
-reportIncompatibleMethodOverride = "warning"
 reportMissingTypeStubs = "none"
 reportPrivateUsage = "none"
 reportUnknownMemberType = "none"
 reportUnnecessaryIsInstance = "warning"
+reportUnnecessaryTypeIgnoreComment = "error"
 reportUnusedImport = "none"
 reportUnusedVariable = "none"
-reportWildcardImportFromLibrary = "none"
 strictParameterNoneValue = false
 typeCheckingMode = "strict"
 useLibraryCodeForTypes = true
@@ -243,6 +243,7 @@ target-version = "py39"  # important to set before applying fixes
 
 [tool.ruff.lint]  # https://docs.astral.sh/ruff/settings/#lint
 extend-safe-fixes = [
+  "UP006",
   "UP007",
   "UP038",
   "UP040",
diff --git a/runway/_cli/commands/_gen_sample/_k8s_cfn_repo.py b/runway/_cli/commands/_gen_sample/_k8s_cfn_repo.py
index da153f799..302f45828 100644
--- a/runway/_cli/commands/_gen_sample/_k8s_cfn_repo.py
+++ b/runway/_cli/commands/_gen_sample/_k8s_cfn_repo.py
@@ -6,7 +6,7 @@
 from typing import TYPE_CHECKING, Any, cast
 
 import click
-from cfn_flip import to_yaml
+from cfn_flip import to_yaml  # pyright: ignore[reportUnknownVariableType]
 
 from ....blueprints.k8s.k8s_iam import Iam
 from ....blueprints.k8s.k8s_master import Cluster
diff --git a/runway/_cli/commands/_gen_sample/utils.py b/runway/_cli/commands/_gen_sample/utils.py
index 4d5ff87cf..8236b73ec 100644
--- a/runway/_cli/commands/_gen_sample/utils.py
+++ b/runway/_cli/commands/_gen_sample/utils.py
@@ -5,7 +5,7 @@
 from pathlib import Path
 
 import click
-from cfn_flip import to_yaml
+from cfn_flip import to_yaml  # pyright: ignore[reportUnknownVariableType]
 from typing_extensions import Literal
 
 from ....blueprints.tf_state import TfState
diff --git a/runway/_cli/commands/_schema/_cfngin.py b/runway/_cli/commands/_schema/_cfngin.py
index 963518b4d..075f4ad75 100644
--- a/runway/_cli/commands/_schema/_cfngin.py
+++ b/runway/_cli/commands/_schema/_cfngin.py
@@ -2,10 +2,11 @@
 
 from __future__ import annotations
 
+import json
 import locale
 import logging
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import click
 
@@ -37,7 +38,7 @@
     metavar="<file-name>",
 )
 @options.verbose
-def cfngin(indent: int, output: Optional[str], **_: Any) -> None:
+def cfngin(indent: int, output: str | None, **_: Any) -> None:
     """Output JSON schema for CFNgin configuration files.
 
     The schema that is output can be used to validate configuration files.
@@ -45,7 +46,7 @@ def cfngin(indent: int, output: Optional[str], **_: Any) -> None:
     and suggestions within configuration files.
 
     """
-    content = CfnginConfigDefinitionModel.schema_json(indent=indent)
+    content = json.dumps(CfnginConfigDefinitionModel.model_json_schema(), indent=indent)
     if output:
         file_path = Path(output).absolute()
         file_path.write_text(  # append empty line to end of file
diff --git a/runway/_cli/commands/_schema/_runway.py b/runway/_cli/commands/_schema/_runway.py
index da33fc37f..0e4e90bd1 100644
--- a/runway/_cli/commands/_schema/_runway.py
+++ b/runway/_cli/commands/_schema/_runway.py
@@ -2,10 +2,11 @@
 
 from __future__ import annotations
 
+import json
 import locale
 import logging
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import click
 
@@ -37,7 +38,7 @@
     metavar="<file-name>",
 )
 @options.verbose
-def runway(indent: int, output: Optional[str], **_: Any) -> None:
+def runway(indent: int, output: str | None, **_: Any) -> None:
     """Output JSON schema Runway configuration files.
 
     The schema that is output can be used to validate configuration files.
@@ -45,7 +46,7 @@ def runway(indent: int, output: Optional[str], **_: Any) -> None:
     and suggestions within configuration files.
 
     """
-    content = RunwayConfigDefinitionModel.schema_json(indent=indent)
+    content = json.dumps(RunwayConfigDefinitionModel.model_json_schema(), indent=indent)
     if output:
         file_path = Path(output).absolute()
         file_path.write_text(  # append empty line to end of file
diff --git a/runway/_cli/commands/_tfenv/_uninstall.py b/runway/_cli/commands/_tfenv/_uninstall.py
index 41a12293a..6a3b522bc 100644
--- a/runway/_cli/commands/_tfenv/_uninstall.py
+++ b/runway/_cli/commands/_tfenv/_uninstall.py
@@ -1,8 +1,10 @@
 """Uninstall Terraform version(s) that were installed by Runway and/or tfenv."""
 
 # docs: file://./../../../../docs/source/commands.rst
+from __future__ import annotations
+
 import logging
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import click
 
@@ -33,7 +35,7 @@
 def uninstall(
     ctx: click.Context,
     *,
-    version: Optional[str] = None,
+    version: str | None = None,
     all_versions: bool = False,
     **_: Any,
 ) -> None:
diff --git a/runway/blueprints/staticsite/auth_at_edge.py b/runway/blueprints/staticsite/auth_at_edge.py
index 1e7bbeb36..fd30bc002 100644
--- a/runway/blueprints/staticsite/auth_at_edge.py
+++ b/runway/blueprints/staticsite/auth_at_edge.py
@@ -8,7 +8,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, ClassVar, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 
 import awacs.logs
 import awacs.s3
@@ -64,8 +64,8 @@ def __init__(
         self,
         name: str,
         context: CfnginContext,
-        mappings: Optional[dict[str, dict[str, Any]]] = None,
-        description: Optional[str] = None,
+        mappings: dict[str, dict[str, Any]] | None = None,
+        description: str | None = None,
     ) -> None:
         """Initialize the Blueprint.
 
@@ -363,8 +363,7 @@ def _get_error_responses(self) -> list[cloudfront.CustomErrorResponse]:
             )
         ]
 
-    # pyright: reportIncompatibleMethodOverride=none
-    def _get_cloudfront_bucket_policy_statements(
+    def _get_cloudfront_bucket_policy_statements(  # pyright: ignore [reportIncompatibleMethodOverride]
         self, bucket: s3.Bucket, oai: cloudfront.CloudFrontOriginAccessIdentity
     ) -> list[Statement]:
         return [
diff --git a/runway/blueprints/staticsite/dependencies.py b/runway/blueprints/staticsite/dependencies.py
index f055462b2..255a8599a 100755
--- a/runway/blueprints/staticsite/dependencies.py
+++ b/runway/blueprints/staticsite/dependencies.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 """Module with static website supporting infrastructure."""
+
 import logging
+from typing import Any
 
 import awacs.s3
 from awacs.aws import Allow, AWSPrincipal, Policy, Statement
@@ -124,7 +126,7 @@ def create_template(self) -> None:
         )
 
         if self.variables["AuthAtEdge"]:
-            userpool_client_params = {
+            userpool_client_params: dict[str, Any] = {
                 "AllowedOAuthFlows": ["code"],
                 "AllowedOAuthScopes": self.variables["OAuthScopes"],
             }
diff --git a/runway/blueprints/staticsite/staticsite.py b/runway/blueprints/staticsite/staticsite.py
index 9722a7383..ed1c2b9de 100755
--- a/runway/blueprints/staticsite/staticsite.py
+++ b/runway/blueprints/staticsite/staticsite.py
@@ -5,7 +5,7 @@
 import hashlib
 import logging
 import os
-from typing import TYPE_CHECKING, Any, ClassVar, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 
 import awacs.awslambda
 import awacs.iam
@@ -271,19 +271,19 @@ def get_cloudfront_distribution_options(
             "ViewerCertificate": self.add_acm_cert(),
         }
 
-    def add_aliases(self) -> Union[list[str], Ref]:
+    def add_aliases(self) -> list[str] | Ref:
         """Add aliases."""
         if self.aliases_specified:
             return self.variables["Aliases"]
         return NoValue
 
-    def add_web_acl(self) -> Union[str, Ref]:
+    def add_web_acl(self) -> list[str] | Ref:
         """Add Web ACL."""
         if self.waf_name_specified:
             return self.variables["WAFWebACL"]
         return NoValue
 
-    def add_logging_bucket(self) -> Union[cloudfront.Logging, Ref]:
+    def add_logging_bucket(self) -> cloudfront.Logging | Ref:
         """Add Logging Bucket."""
         if self.cf_logging_enabled:
             return cloudfront.Logging(
@@ -291,7 +291,7 @@ def add_logging_bucket(self) -> Union[cloudfront.Logging, Ref]:
             )
         return NoValue
 
-    def add_acm_cert(self) -> Union[cloudfront.ViewerCertificate, Ref]:
+    def add_acm_cert(self) -> cloudfront.ViewerCertificate | Ref:
         """Add ACM cert."""
         if self.acm_certificate_specified:
             return cloudfront.ViewerCertificate(
diff --git a/runway/cfngin/actions/base.py b/runway/cfngin/actions/base.py
index 522a9c330..2937e1899 100644
--- a/runway/cfngin/actions/base.py
+++ b/runway/cfngin/actions/base.py
@@ -6,7 +6,7 @@
 import os
 import sys
 import threading
-from typing import TYPE_CHECKING, Any, Callable, ClassVar, Union
+from typing import TYPE_CHECKING, Any, Callable, ClassVar
 
 import botocore.exceptions
 
@@ -172,21 +172,17 @@ def execute(self, **kwargs: Any) -> None:
             LOGGER.error(str(err))
             sys.exit(1)
 
-    def pre_run(
-        self, *, dump: Union[bool, str] = False, outline: bool = False, **__kwargs: Any
-    ) -> None:
+    def pre_run(self, *, dump: bool | str = False, outline: bool = False, **__kwargs: Any) -> None:
         """Perform steps before running the action."""
 
-    def post_run(
-        self, *, dump: Union[bool, str] = False, outline: bool = False, **__kwargs: Any
-    ) -> None:
+    def post_run(self, *, dump: bool | str = False, outline: bool = False, **__kwargs: Any) -> None:
         """Perform steps after running the action."""
 
     def run(
         self,
         *,
         concurrency: int = 0,
-        dump: Union[bool, str] = False,
+        dump: bool | str = False,
         force: bool = False,
         outline: bool = False,
         tail: bool = False,
@@ -211,9 +207,7 @@ def s3_stack_push(self, blueprint: Blueprint, force: bool = False) -> str:
         key_name = stack_template_key_name(blueprint)
         template_url = self.stack_template_url(blueprint)
         try:
-            template_exists = (
-                self.s3_conn.head_object(Bucket=self.bucket_name, Key=key_name) is not None
-            )
+            template_exists = bool(self.s3_conn.head_object(Bucket=self.bucket_name, Key=key_name))
         except botocore.exceptions.ClientError as err:
             if err.response["Error"]["Code"] == "404":
                 template_exists = False
diff --git a/runway/cfngin/actions/deploy.py b/runway/cfngin/actions/deploy.py
index cdb031669..0550957f7 100644
--- a/runway/cfngin/actions/deploy.py
+++ b/runway/cfngin/actions/deploy.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Callable, Union
+from typing import TYPE_CHECKING, Any, Callable
 
 from ..exceptions import (
     CancelExecution,
@@ -185,7 +185,7 @@ def handle_hooks(
     provider: Provider,
     context: CfnginContext,
     *,
-    dump: Union[bool, str] = False,
+    dump: bool | str = False,
     outline: bool = False,
 ) -> None:
     """Handle pre/post hooks.
@@ -602,7 +602,7 @@ def run(
         if isinstance(dump, str):
             plan.dump(directory=dump, context=self.context, provider=self.provider)
 
-    def post_run(self, *, dump: Union[bool, str] = False, outline: bool = False, **_: Any) -> None:
+    def post_run(self, *, dump: bool | str = False, outline: bool = False, **_: Any) -> None:
         """Any steps that need to be taken after running the action."""
         handle_hooks(
             "post_deploy",
diff --git a/runway/cfngin/actions/diff.py b/runway/cfngin/actions/diff.py
index e63ec16d5..7deaae5b5 100644
--- a/runway/cfngin/actions/diff.py
+++ b/runway/cfngin/actions/diff.py
@@ -11,7 +11,6 @@
     Callable,
     Generic,
     TypeVar,
-    Union,
     cast,
 )
 
@@ -291,7 +290,5 @@ def pre_run(
             LOGGER.verbose("proceeding without a cfngin_bucket...")
             self.bucket_name = None
 
-    def post_run(
-        self, *, dump: Union[bool, str] = False, outline: bool = False, **__kwargs: Any
-    ) -> None:
+    def post_run(self, *, dump: bool | str = False, outline: bool = False, **__kwargs: Any) -> None:
         """Do nothing."""
diff --git a/runway/cfngin/blueprints/base.py b/runway/cfngin/blueprints/base.py
index d9da75435..efe5fbe27 100644
--- a/runway/cfngin/blueprints/base.py
+++ b/runway/cfngin/blueprints/base.py
@@ -99,7 +99,7 @@ def build_parameter(name: str, properties: BlueprintVariableTypeDef) -> Paramete
     param = Parameter(name, Type=properties.get("type"))
     for name_, attr in PARAMETER_PROPERTIES.items():
         if name_ in properties:
-            setattr(param, attr, properties[name_])  # type: ignore
+            setattr(param, attr, properties[name_])
     return param
 
 
@@ -185,10 +185,9 @@ def resolve_variable(
             original exception.
 
     """
-    try:
-        var_type = var_def["type"]
-    except KeyError:
-        raise VariableTypeRequired(blueprint_name, var_name) from None
+    if "type" not in var_def:
+        raise VariableTypeRequired(blueprint_name, var_name)
+    var_type = var_def["type"]
 
     if provided_variable:
         if not provided_variable.resolved:
@@ -198,10 +197,9 @@ def resolve_variable(
     else:
         # Variable value not provided, try using the default, if it exists
         # in the definition
-        try:
-            value = var_def["default"]
-        except KeyError:
-            raise MissingVariable(blueprint_name, var_name) from None
+        if "default" not in var_def:
+            raise MissingVariable(blueprint_name, var_name)
+        value = var_def["default"]
 
     # If no validator, return the value as is, otherwise apply validator
     validator = var_def.get("validator", lambda v: v)
diff --git a/runway/cfngin/blueprints/cfngin_bucket.py b/runway/cfngin/blueprints/cfngin_bucket.py
index ebd9dff8e..e34979e93 100644
--- a/runway/cfngin/blueprints/cfngin_bucket.py
+++ b/runway/cfngin/blueprints/cfngin_bucket.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, ClassVar, Union
+from typing import TYPE_CHECKING, ClassVar
 
 from troposphere import Equals, If, Not, NoValue, Or, Tag, Tags, s3
 
@@ -70,7 +70,7 @@ def bucket(self) -> s3.Bucket:
         return bucket
 
     @cached_property
-    def bucket_encryption(self) -> Union[AWSHelperFn, s3.BucketEncryption]:
+    def bucket_encryption(self) -> AWSHelperFn | s3.BucketEncryption:
         """CFNgin bucket encryption.
 
         This cached property can be overridden in a subclass to customize the
diff --git a/runway/cfngin/blueprints/raw.py b/runway/cfngin/blueprints/raw.py
index 3218eba77..a7637a43f 100644
--- a/runway/cfngin/blueprints/raw.py
+++ b/runway/cfngin/blueprints/raw.py
@@ -7,7 +7,7 @@
 import logging
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Union
+from typing import TYPE_CHECKING, Any
 
 from jinja2 import Environment, FileSystemLoader
 
@@ -141,7 +141,7 @@ def parameter_definitions(self) -> dict[str, Any]:
         return self.to_dict().get("Parameters", {})
 
     @cached_property
-    def parameter_values(self) -> dict[str, Union[list[Any], str]]:
+    def parameter_values(self) -> dict[str, list[Any] | str]:
         """Return a dict of variables with type :class:`~runway.cfngin.blueprints.variables.types.CFNType`.
 
         .. versionadded:: 2.0.0
diff --git a/runway/cfngin/blueprints/variables/types.py b/runway/cfngin/blueprints/variables/types.py
index d4b027f11..6d1e833be 100644
--- a/runway/cfngin/blueprints/variables/types.py
+++ b/runway/cfngin/blueprints/variables/types.py
@@ -147,7 +147,7 @@ class CFNType:
 
     """
 
-    parameter_type: ClassVar[str]
+    parameter_type: ClassVar
 
 
 # General CFN types
diff --git a/runway/cfngin/dag/__init__.py b/runway/cfngin/dag/__init__.py
index e962973ed..9ccfd7c7e 100644
--- a/runway/cfngin/dag/__init__.py
+++ b/runway/cfngin/dag/__init__.py
@@ -9,7 +9,7 @@
 from collections import OrderedDict
 from copy import copy, deepcopy
 from threading import Thread
-from typing import TYPE_CHECKING, Any, Callable, Union, cast
+from typing import TYPE_CHECKING, Any, Callable, cast
 
 if TYPE_CHECKING:
     import threading
@@ -288,7 +288,7 @@ def all_leaves(self) -> list[str]:
         graph = self.graph
         return [key for key in graph if not graph[key]]
 
-    def from_dict(self, graph_dict: dict[str, Union[Iterable[str], Any]]) -> None:
+    def from_dict(self, graph_dict: dict[str, Iterable[str] | Any]) -> None:
         """Reset the graph and build it from the passed dictionary.
 
         The dictionary takes the form of {node_name: [directed edges]}
@@ -390,7 +390,7 @@ def release(self) -> Any:
 class ThreadedWalker:
     """Walk a DAG as quickly as the graph topology allows, using threads."""
 
-    def __init__(self, semaphore: Union[threading.Semaphore, UnlimitedSemaphore]) -> None:
+    def __init__(self, semaphore: threading.Semaphore | UnlimitedSemaphore) -> None:
         """Instantiate class.
 
         Args:
diff --git a/runway/cfngin/hooks/acm.py b/runway/cfngin/hooks/acm.py
index 5b34c8067..347775cbe 100644
--- a/runway/cfngin/hooks/acm.py
+++ b/runway/cfngin/hooks/acm.py
@@ -4,7 +4,7 @@
 
 import logging
 import time
-from typing import TYPE_CHECKING, Any, ClassVar, List, Optional  # noqa: UP035
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from botocore.exceptions import ClientError
 from troposphere import Ref
@@ -25,9 +25,7 @@
     from typing_extensions import Literal
 
     from ...context import CfnginContext
-    from ..blueprints.base import Blueprint
     from ..providers.aws.default import Provider
-    from ..stack import Stack
     from ..status import Status
 
 LOGGER = logging.getLogger(__name__)
@@ -36,10 +34,10 @@
 class HookArgs(HookArgsBaseModel):
     """Hook arguments."""
 
-    alt_names: List[str] = []  # noqa: UP006
+    alt_names: list[str] = []
     domain: str
     hosted_zone_id: str
-    stack_name: Optional[str] = None
+    stack_name: str | None = None
     ttl: int = 300
 
 
@@ -47,7 +45,7 @@ class Certificate(Hook):
     r"""Hook for managing a **AWS::CertificateManager::Certificate**.
 
     Keyword Args:
-        alt_names (Optional[list[str]]): Additional FQDNs to be included in the
+        alt_names (list[str]): Additional FQDNs to be included in the
             Subject Alternative Name extension of the ACM certificate. For
             example, you can add www.example.net to a certificate for which the
             domain field is www.example.com if users can reach your site by
@@ -61,10 +59,10 @@ class Certificate(Hook):
         hosted_zone_id (str): The ID of the Route 53 Hosted Zone that contains
             the resource record sets that you want to change. This must exist
             in the same account that the certificate will be created in.
-        stack_name (Optional[str]): Provide a name for the stack used to
+        stack_name (str | None): Provide a name for the stack used to
             create the certificate. If not provided, the domain is used
             (replacing ``.`` with ``-``).
-        ttl (Optional[int]): The resource record cache time to live (TTL),
+        ttl (int): The resource record cache time to live (TTL),
             in seconds. (*default:* ``300``)
 
     .. rubric:: Example
@@ -84,9 +82,7 @@ class Certificate(Hook):
 
     acm_client: ACMClient
     args: HookArgs
-    blueprint: Blueprint
     r53_client: Route53Client
-    stack: Stack
     template_description: str
 
     def __init__(self, context: CfnginContext, provider: Provider, **kwargs: Any) -> None:
@@ -121,7 +117,7 @@ def __init__(self, context: CfnginContext, provider: Provider, **kwargs: Any) ->
             }
         )
 
-    def _create_blueprint(self) -> Blueprint:
+    def _create_blueprint(self) -> BlankBlueprint:
         """Create CFNgin Blueprint."""
         var_description = (
             "NO NOT CHANGE MANUALLY! Used to track the "
@@ -152,6 +148,8 @@ class _BlankBlueprint(BlankBlueprint):
 
     def domain_changed(self) -> bool:
         """Check to ensure domain has not changed for existing stack."""
+        if not self.stack:  # cov: ignore
+            raise NotImplementedError("stack not present on hook")
         try:
             stack_info = self.provider.get_stack(self.stack.fqn)
             if self.provider.is_stack_recreatable(stack_info):
@@ -187,6 +185,8 @@ def get_certificate(self, interval: int = 5) -> str:
             Certificate ARN.
 
         """
+        if not self.stack:  # cov: ignore
+            raise NotImplementedError("stack not present on hook")
         response = self.provider.cloudformation.describe_stack_resources(
             StackName=self.stack.fqn, LogicalResourceId="Certificate"
         )["StackResources"]
@@ -199,7 +199,7 @@ def get_certificate(self, interval: int = 5) -> str:
 
     def get_validation_record(
         self,
-        cert_arn: Optional[str] = None,
+        cert_arn: str | None = None,
         *,
         interval: int = 5,
         status: str = "PENDING_VALIDATION",
@@ -220,17 +220,17 @@ def get_validation_record(
             cert_arn = self.get_certificate()
         cert = self.acm_client.describe_certificate(CertificateArn=cert_arn).get("Certificate", {})
 
-        try:
-            domain_validation = [
-                opt for opt in cert["DomainValidationOptions"] if opt["ValidationStatus"] == status
-            ]
-        except KeyError:
+        if "DomainValidationOptions" not in cert:
             LOGGER.debug(
                 "waiting for DomainValidationOptions to become available for the certificate..."
             )
             time.sleep(interval)
             return self.get_validation_record(cert_arn=cert_arn, interval=interval, status=status)
 
+        domain_validation = [
+            opt for opt in cert["DomainValidationOptions"] if opt.get("ValidationStatus") == status
+        ]
+
         if not domain_validation:
             raise ValueError(
                 f'No validations with status "{status}" found for "{self.args.domain}"'
@@ -240,16 +240,14 @@ def get_validation_record(
                 f"Found {len(domain_validation)} validation options of status "
                 f'"{status}" for "{self.args.domain}"; only one option is supported'
             )
-        try:
-            # the validation option can exists before the record set is ready
+        if "ResourceRecord" in domain_validation[0]:
             return domain_validation[0]["ResourceRecord"]
-        except KeyError:
-            LOGGER.debug(
-                "waiting for DomainValidationOptions.ResourceRecord "
-                "to become available for the certificate..."
-            )
-            time.sleep(interval)
-            return self.get_validation_record(cert_arn=cert_arn, interval=interval, status=status)
+        LOGGER.debug(
+            "waiting for DomainValidationOptions.ResourceRecord to become available "
+            "for the certificate..."
+        )
+        time.sleep(interval)
+        return self.get_validation_record(cert_arn=cert_arn, interval=interval, status=status)
 
     def put_record_set(self, record_set: ResourceRecordTypeDef) -> None:
         """Create/update a record set on a Route 53 Hosted Zone.
@@ -261,9 +259,7 @@ def put_record_set(self, record_set: ResourceRecordTypeDef) -> None:
         LOGGER.info("adding validation record to hosted zone: %s", self.args.hosted_zone_id)
         self.__change_record_set("CREATE", [record_set])
 
-    def remove_validation_records(
-        self, records: Optional[list[ResourceRecordTypeDef]] = None
-    ) -> None:
+    def remove_validation_records(self, records: list[ResourceRecordTypeDef] | None = None) -> None:
         """Remove all record set entries used to validate an ACM Certificate.
 
         Args:
@@ -339,7 +335,7 @@ def __change_record_set(
             ChangeBatch={"Comment": self.template_description, "Changes": changes},
         )
 
-    def deploy(self, status: Optional[Status] = None) -> dict[str, str]:
+    def deploy(self, status: Status | None = None) -> dict[str, str]:
         """Deploy an ACM Certificate."""
         record = None
         try:
@@ -389,7 +385,7 @@ def deploy(self, status: Optional[Status] = None) -> dict[str, str]:
             LOGGER.error(err)
             self.destroy(
                 records=[record] if record else None,
-                skip_r53=isinstance(  # type: ignore
+                skip_r53=isinstance(
                     err,
                     (
                         self.r53_client.exceptions.InvalidChangeBatch,
@@ -404,7 +400,7 @@ def deploy(self, status: Optional[Status] = None) -> dict[str, str]:
 
     def destroy(
         self,
-        records: Optional[list[ResourceRecordTypeDef]] = None,
+        records: list[ResourceRecordTypeDef] | None = None,
         skip_r53: bool = False,
     ) -> bool:
         """Destroy an ACM certificate.
@@ -416,6 +412,8 @@ def destroy(
             skip_r53: Skip the removal of validation records.
 
         """
+        if not self.stack:  # cov: ignore
+            raise NotImplementedError("stack not present on hook")
         if not skip_r53:
             try:
                 self.remove_validation_records(records)
diff --git a/runway/cfngin/hooks/aws_lambda.py b/runway/cfngin/hooks/aws_lambda.py
index 48286851e..00c515f8e 100644
--- a/runway/cfngin/hooks/aws_lambda.py
+++ b/runway/cfngin/hooks/aws_lambda.py
@@ -12,15 +12,12 @@
 import subprocess
 import sys
 import tempfile
-from collections.abc import Iterable, Iterator
 from io import BytesIO as StringIO
 from pathlib import Path
 from shutil import copyfile
 from typing import (
     TYPE_CHECKING,
     Any,
-    Optional,
-    Union,
     cast,
 )
 from zipfile import ZIP_DEFLATED, ZipFile
@@ -30,7 +27,6 @@
 import docker
 import docker.types
 import formic
-from docker.models.images import Image
 from troposphere.awslambda import Code
 from typing_extensions import Literal, TypedDict
 
@@ -38,9 +34,13 @@
 from ..utils import ensure_s3_bucket
 
 if TYPE_CHECKING:
+    from collections.abc import Iterable, Iterator
+
+    from docker.models.images import Image
     from mypy_boto3_s3.client import S3Client
     from mypy_boto3_s3.literals import ObjectCannedACLType
     from mypy_boto3_s3.type_defs import HeadObjectOutputTypeDef
+    from typing_extensions import TypeAlias
 
     from ...context import CfnginContext
     from ..providers.aws.default import Provider
@@ -54,19 +54,16 @@
 # list from python tags of https://hub.docker.com/r/lambci/lambda/tags
 SUPPORTED_RUNTIMES = ["python3.7", "python3.8"]
 
-DockerizePipArgTypeDef = Optional[
-    Union[
-        bool,
-        Literal["false", "False", "no", "No", "non-linux", "true", "True", "yes", "Yes"],
-    ]
-]
+DockerizePipArgTypeDef: TypeAlias = (
+    'bool | Literal["false", "False", "no", "No", "non-linux", "true", "True", "yes", "Yes"] | None'
+)
 
 
 def copydir(
     source: str,
     destination: str,
     includes: list[str],
-    excludes: Optional[list[str]] = None,
+    excludes: list[str] | None = None,
     follow_symlinks: bool = False,
 ) -> None:
     """Extend the functionality of shutil.
@@ -103,7 +100,7 @@ def _mkdir(dir_name: str) -> None:
             copyfile(src, dest)
 
 
-def find_requirements(root: str) -> Optional[dict[str, bool]]:
+def find_requirements(root: str) -> dict[str, bool] | None:
     """Identify Python requirement files.
 
     Args:
@@ -212,8 +209,8 @@ def _calculate_hash(files: Iterable[str], root: str) -> str:
 
 def _find_files(
     root: str,
-    includes: Union[list[str], str],
-    excludes: Optional[list[str]] = None,
+    includes: list[str] | str,
+    excludes: list[str] | None = None,
     follow_symlinks: bool = False,
 ) -> Iterator[str]:
     """List files inside a directory based on include and exclude rules.
@@ -336,7 +333,7 @@ def handle_requirements(
 def _handle_use_pipenv(
     package_root: str,
     dest_path: str,
-    python_path: Optional[str] = None,
+    python_path: str | None = None,
     timeout: int = 300,
 ) -> str:
     """Create requirements file from Pipfile.
@@ -377,10 +374,10 @@ def _handle_use_pipenv(
 
 def dockerized_pip(  # noqa: C901, PLR0912
     work_dir: str,
-    client: Optional[docker.DockerClient] = None,
-    runtime: Optional[str] = None,
-    docker_file: Optional[str] = None,
-    docker_image: Optional[str] = None,
+    client: docker.DockerClient | None = None,
+    runtime: str | None = None,
+    docker_file: str | None = None,
+    docker_image: str | None = None,
     python_dontwritebytecode: bool = False,
     **_: Any,
 ) -> None:
@@ -414,7 +411,7 @@ def dockerized_pip(  # noqa: C901, PLR0912
             raise ValueError(f'could not find docker_file "{docker_file}"')
         LOGGER.info('building docker image from "%s"', docker_file)
         response = cast(
-            Union[Image, tuple[Image, Iterator[dict[str, str]]]],
+            "Image | tuple[Image, Iterator[dict[str, str]]]",
             client.images.build(
                 path=os.path.dirname(docker_file),  # noqa: PTH120
                 dockerfile=os.path.basename(docker_file),  # noqa: PTH119
@@ -506,14 +503,14 @@ def _zip_package(  # noqa: PLR0915, PLR0912, C901, D417
     package_root: str,
     *,
     dockerize_pip: DockerizePipArgTypeDef = False,
-    excludes: Optional[list[str]] = None,
+    excludes: list[str] | None = None,
     follow_symlinks: bool = False,
     includes: list[str],
     pipenv_timeout: int = 300,
     python_dontwritebytecode: bool = False,
     python_exclude_bin_dir: bool = False,
     python_exclude_setuptools_dirs: bool = False,
-    python_path: Optional[str] = None,
+    python_path: str | None = None,
     requirements_files: dict[str, bool],
     use_pipenv: bool = False,
     work_dir: Path,
@@ -649,7 +646,7 @@ def _zip_package(  # noqa: PLR0915, PLR0912, C901, D417
     return contents, content_hash
 
 
-def _head_object(s3_conn: S3Client, bucket: str, key: str) -> Optional[HeadObjectOutputTypeDef]:
+def _head_object(s3_conn: S3Client, bucket: str, key: str) -> HeadObjectOutputTypeDef | None:
     """Retrieve information about an object in S3 if it exists.
 
     Args:
@@ -679,7 +676,7 @@ def _upload_code(
     bucket: str,
     prefix: str,
     name: str,
-    contents: Union[bytes, str],
+    contents: bytes | str,
     content_hash: str,
     payload_acl: ObjectCannedACLType,
 ) -> Code:
@@ -727,10 +724,10 @@ def _upload_code(
 
 
 def _check_pattern_list(
-    patterns: Optional[Union[list[str], str]],
+    patterns: list[str] | str | None,
     key: str,
-    default: Optional[list[str]] = None,
-) -> Optional[list[str]]:
+    default: list[str] | None = None,
+) -> list[str] | None:
     """Validate file search patterns from user configuration.
 
     Acceptable input is a string (which will be converted to a singleton list),
@@ -774,8 +771,8 @@ class _UploadFunctionOptionsTypeDef(TypedDict):
 
     """
 
-    exclude: Optional[list[str]]
-    include: Optional[list[str]]
+    exclude: list[str] | None
+    include: list[str] | None
     path: str
 
 
@@ -850,9 +847,9 @@ def _upload_function(
 
 
 def select_bucket_region(
-    custom_bucket: Optional[str],
-    hook_region: Optional[str],
-    cfngin_bucket_region: Optional[str],
+    custom_bucket: str | None,
+    hook_region: str | None,
+    cfngin_bucket_region: str | None,
     provider_region: str,
 ) -> str:
     """Return the appropriate region to use when uploading functions.
@@ -902,41 +899,41 @@ def upload_lambda_functions(  # noqa: D417
         context: Context instance. (passed in by CFNgin)
 
     Keyword Args:
-        bucket (Optional[str]): Custom bucket to upload functions to.
+        bucket (str | None): Custom bucket to upload functions to.
             Omitting it will cause the default CFNgin bucket to be used.
-        bucket_region (Optional[str]): The region in which the bucket should
+        bucket_region (str | None): The region in which the bucket should
             exist. If not given, the region will be either be that of the
             global ``cfngin_bucket_region`` setting, or else the region in
             use by the provider.
-        prefix (Optional[str]): S3 key prefix to prepend to the uploaded
+        prefix (str | None): S3 key prefix to prepend to the uploaded
             zip name.
-        follow_symlinks (Optional[bool]): Will determine if symlinks should
+        follow_symlinks (str | None): Will determine if symlinks should
             be followed and included with the zip artifact. (*default:*
             ``False``)
-        payload_acl (Optional[str]): The canned S3 object ACL to be applied
+        payload_acl (str | None): The canned S3 object ACL to be applied
             to the uploaded payload. (*default: private*)
         functions (dict[str, Any]): Configurations of desired payloads to
             build. Keys correspond to function names, used to derive key
             names for the payload. Each value should itself be a dictionary,
             with the following data:
 
-            **docker_file (Optional[str])**
+            **docker_file (str | None)**
                 Path to a local DockerFile that will be built and used for
                 ``dockerize_pip``. Must provide exactly one of ``docker_file``,
                 ``docker_image``, or ``runtime``.
 
-            **docker_image (Optional[str])**
+            **docker_image (str | None)**
                 Custom Docker image to use  with ``dockerize_pip``. Must
                 provide exactly one of ``docker_file``, ``docker_image``, or
                 ``runtime``.
 
-            **dockerize_pip (Optional[Union[str, bool]])**
+            **dockerize_pip (bool | str | None)**
                 Whether to use Docker when preparing package dependencies with
                 pip. Can be set to True/False or the special string 'non-linux'
                 which will only run on non Linux systems. To use this option
                 Docker must be installed.
 
-            **exclude (Optional[Union[str, list[str]]])**
+            **exclude (str | list[str] | None)**
                 Pattern or list of patterns of files to exclude from the
                 payload. If provided, any files that match will be ignored,
                 regardless of whether they match an inclusion pattern.
@@ -945,7 +942,7 @@ def upload_lambda_functions(  # noqa: D417
                 such as ``.git``, ``.svn``, ``__pycache__``, ``*.pyc``,
                 ``.gitignore``, etc.
 
-            **include (Optional[Union[str, list[str]]])**
+            **include (str | list[str] | None)**
                 Pattern or list of patterns of files to include in the
                 payload. If provided, only files that match these
                 patterns will be included in the payload.
@@ -969,25 +966,25 @@ def upload_lambda_functions(  # noqa: D417
                 directly under this directory will be added to the root of
                 the ZIP file.
 
-            **pipenv_lock_timeout (Optional[int])**
+            **pipenv_lock_timeout (int | None)**
                 Time in seconds to wait while creating lock file with pipenv.
 
-            **pipenv_timeout (Optional[int])**
+            **pipenv_timeout (int | None)**
                 Time in seconds to wait while running pipenv.
 
-            **python_path (Optional[str])**
+            **python_path (str | None)**
                 Absolute path to a python interpreter to use for
                 ``pip``/``pipenv`` actions. If not provided, the current
                 python interpreter will be used for ``pip`` and ``pipenv``
                 will be used from the current ``$PATH``.
 
-            **runtime (Optional[str])**
+            **runtime (str | None)**
                 Runtime of the AWS Lambda Function being uploaded. Used with
                 ``dockerize_pip`` to automatically select the appropriate
                 Docker image to use. Must provide exactly one of
                 ``docker_file``, ``docker_image``, or ``runtime``.
 
-            **use_pipenv (Optional[bool])**
+            **use_pipenv (bool)**
                 Explicitly use Pipfile/Pipfile.lock to prepare package
                 dependencies even if a requirements.txt file is found.
 
diff --git a/runway/cfngin/hooks/awslambda/_python_hooks.py b/runway/cfngin/hooks/awslambda/_python_hooks.py
index 303d96995..c1888af2a 100644
--- a/runway/cfngin/hooks/awslambda/_python_hooks.py
+++ b/runway/cfngin/hooks/awslambda/_python_hooks.py
@@ -29,7 +29,7 @@ class PythonFunction(AwsLambdaHook[PythonProject]):
     def __init__(self, context: CfnginContext, **kwargs: Any) -> None:
         """Instantiate class."""
         super().__init__(context)
-        self.args = PythonHookArgs.parse_obj(kwargs)
+        self.args = PythonHookArgs.model_validate(kwargs)
 
     @cached_property
     def deployment_package(self) -> DeploymentPackage[PythonProject]:
@@ -56,7 +56,7 @@ def pre_deploy(self) -> Any:
         """Run during the **pre_deploy** stage."""
         try:
             self.deployment_package.upload()
-            return self.build_response("deploy").dict(by_alias=True)
+            return self.build_response("deploy").model_dump(by_alias=True)
         except BaseException:
             self.cleanup_on_error()
             raise
diff --git a/runway/cfngin/hooks/awslambda/base_classes.py b/runway/cfngin/hooks/awslambda/base_classes.py
index a15e30e4e..9cd2501cc 100644
--- a/runway/cfngin/hooks/awslambda/base_classes.py
+++ b/runway/cfngin/hooks/awslambda/base_classes.py
@@ -310,9 +310,6 @@ class AwsLambdaHook(CfnginHookProtocol, Generic[_ProjectTypeVar]):
     BUILD_LAYER: ClassVar[bool] = False
     """Flag to denote if the hook creates a Lambda Function or Layer deployment package."""
 
-    args: AwsLambdaHookArgs
-    """Parsed hook arguments."""
-
     ctx: CfnginContext
     """CFNgin context object."""
 
@@ -452,7 +449,7 @@ def plan(self) -> AwsLambdaHookDeployResponseTypedDict:
         """Run during the **plan** stage."""
         return cast(
             "AwsLambdaHookDeployResponseTypedDict",
-            self.build_response("plan").dict(by_alias=True),
+            self.build_response("plan").model_dump(by_alias=True),
         )
 
     def post_deploy(self) -> Any:
diff --git a/runway/cfngin/hooks/awslambda/deployment_package.py b/runway/cfngin/hooks/awslambda/deployment_package.py
index 9c25b0842..335488134 100644
--- a/runway/cfngin/hooks/awslambda/deployment_package.py
+++ b/runway/cfngin/hooks/awslambda/deployment_package.py
@@ -411,7 +411,11 @@ def upload(self, *, build: bool = True) -> None:
             ContentMD5=self.md5_checksum,
             Key=self.object_key,
             Tagging=self.build_tag_set(),
-            **{"ContentType": content_type} if content_type else {},
+            **(
+                {"ContentType": content_type}  # pyright: ignore[reportArgumentType]
+                if content_type
+                else {}
+            ),
         )
         # clear cached properties so they can recalculate
         self._del_cached_property("object_version_id")
diff --git a/runway/cfngin/hooks/awslambda/docker.py b/runway/cfngin/hooks/awslambda/docker.py
index 1dc1cf6b2..98ad810fd 100644
--- a/runway/cfngin/hooks/awslambda/docker.py
+++ b/runway/cfngin/hooks/awslambda/docker.py
@@ -5,7 +5,7 @@
 import logging
 import os
 import platform
-from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from docker import DockerClient
 from docker.errors import DockerException, ImageNotFound
@@ -21,6 +21,7 @@
     from pathlib import Path
 
     from docker.models.images import Image
+    from typing_extensions import Self
 
     from ...._logging import RunwayLogger
     from ....context import CfnginContext, RunwayContext
@@ -30,9 +31,6 @@
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__))
 
 
-_T = TypeVar("_T")
-
-
 class DockerDependencyInstaller:
     """Docker dependency installer."""
 
@@ -340,7 +338,7 @@ def run_command(self, command: str, *, level: int = logging.INFO) -> list[str]:
                 raise DockerExecFailedError(response)
 
     @classmethod
-    def from_project(cls: type[_T], project: Project[AwsLambdaHookArgs]) -> _T | None:
+    def from_project(cls: type[Self], project: Project[AwsLambdaHookArgs]) -> Self | None:
         """Instantiate class from a project.
 
         High-level method that wraps instantiation in error handling.
diff --git a/runway/cfngin/hooks/awslambda/models/args.py b/runway/cfngin/hooks/awslambda/models/args.py
index 7cc54ddcb..c6fcd837c 100644
--- a/runway/cfngin/hooks/awslambda/models/args.py
+++ b/runway/cfngin/hooks/awslambda/models/args.py
@@ -1,12 +1,11 @@
 """Argument data models."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
 from pathlib import Path
-from typing import Any, List, Optional
+from typing import Annotated
 
-from pydantic import DirectoryPath, Extra, Field, FilePath, validator
+from pydantic import ConfigDict, DirectoryPath, Field, FilePath, ValidationInfo, field_validator
 
 from .....config.models.utils import resolve_path_field
 from .....utils import BaseModel
@@ -16,6 +15,8 @@
 class DockerOptions(BaseModel):
     """Docker options."""
 
+    model_config = ConfigDict(extra="ignore")
+
     disabled: bool = False
     """Explicitly disable the use of docker (default ``False``).
 
@@ -30,7 +31,7 @@ class DockerOptions(BaseModel):
 
     """
 
-    extra_files: List[str] = []
+    extra_files: list[str] = []
     """List of absolute file paths within the Docker container to copy into the
     deployment package.
 
@@ -54,7 +55,7 @@ class DockerOptions(BaseModel):
 
     """
 
-    file: Optional[FilePath] = None
+    file: FilePath | None = None
     """Dockerfile to use to build an image for use in this process.
 
     This, ``image`` , or ``runtime`` must be provided.
@@ -69,7 +70,7 @@ class DockerOptions(BaseModel):
 
     """
 
-    image: Optional[str] = None
+    image: str | None = None
     """Docker image to use. If the image does not exist locally, it will be pulled.
 
     This, ``file`` (takes precedence), or ``runtime`` must be provided.
@@ -85,7 +86,7 @@ class DockerOptions(BaseModel):
 
     """
 
-    name: Optional[str] = None
+    name: str | None = None
     """When providing a Dockerfile, this will be the name applied to the resulting image.
     It is the equivalent to ``name`` in the ``name:tag`` syntax of the
     ``docker build [--tag, -t]`` command option.
@@ -118,12 +119,7 @@ class DockerOptions(BaseModel):
 
     """
 
-    class Config:
-        """Model configuration."""
-
-        extra = Extra.ignore
-
-    _resolve_path_fields = validator("file", allow_reuse=True)(resolve_path_field)  # type: ignore
+    _resolve_path_fields = field_validator("file")(resolve_path_field)
 
 
 class AwsLambdaHookArgs(HookArgsBaseModel):
@@ -135,13 +131,13 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
 
     """
 
-    cache_dir: Optional[Path] = None
+    cache_dir: Path | None = None
     """Explicitly define the directory location.
     Must be an absolute path or it will be relative to the CFNgin module directory.
 
     """
 
-    compatible_architectures: Optional[List[str]] = None
+    compatible_architectures: list[str] | None = None
     """A list of compatible instruction set architectures.
     (https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html)
 
@@ -157,7 +153,7 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
 
     """
 
-    compatible_runtimes: Optional[List[str]] = None
+    compatible_runtimes: list[str] | None = None
     """A list of compatible function runtimes.
     When provided, the ``runtime`` being used to build the deployment package
     must be included in the list or an error will be raised.
@@ -177,7 +173,7 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
     docker: DockerOptions = DockerOptions()
     """Docker options."""
 
-    extend_gitignore: List[str] = []
+    extend_gitignore: list[str] = []
     """gitignore rules that should be added to the rules already defined in a
     ``.gitignore`` file in the source code directory.
     This can be used with or without an existing file.
@@ -199,7 +195,7 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
 
     """
 
-    license: Optional[str] = Field(default=None, max_length=256)
+    license: Annotated[str | None, Field(max_length=256)] = None
     """The layer's software license. Can be any of the following:
 
     - A SPDX license identifier (e.g. ``Apache-2.0``).
@@ -217,7 +213,7 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
 
     """
 
-    object_prefix: Optional[str] = None
+    object_prefix: str | None = None
     """Prefix to add to the S3 Object key.
 
     The object will always be prefixed with ``awslambda/functions``.
@@ -226,7 +222,7 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
 
     """
 
-    runtime: Optional[str] = None
+    runtime: Annotated[str | None, Field(validate_default=True)] = None
     """Runtime of the Lambda Function
     (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html).
 
@@ -264,16 +260,15 @@ class AwsLambdaHookArgs(HookArgsBaseModel):
     use_cache: bool = True
     """Whether to use a cache directory with pip that will persist builds (default ``True``)."""
 
-    _resolve_path_fields = validator("cache_dir", "source_code", allow_reuse=True)(resolve_path_field)  # type: ignore
+    _resolve_path_fields = field_validator("cache_dir", "source_code")(resolve_path_field)
 
-    @validator("runtime", always=True, allow_reuse=True)  # type: ignore
-    def _validate_runtime_or_docker(
-        cls, v: str | None, values: dict[str, Any]  # noqa: N805
-    ) -> str | None:
+    @field_validator("runtime", mode="before")
+    @classmethod
+    def _validate_runtime_or_docker(cls, v: str | None, info: ValidationInfo) -> str | None:
         """Validate that either runtime is provided or Docker image is provided."""
         if v:  # if runtime was provided, we don't need to check anything else
             return v
-        docker: DockerOptions = values["docker"]
+        docker: DockerOptions = info.data["docker"]
         if docker.disabled:
             raise ValueError("runtime must be provided if docker.disabled is True")
         if not (docker.file or docker.image):
@@ -284,7 +279,9 @@ def _validate_runtime_or_docker(
 class PythonHookArgs(AwsLambdaHookArgs):
     """Hook arguments for a Python AWS Lambda deployment package."""
 
-    extend_pip_args: Optional[List[str]] = None
+    model_config = ConfigDict(extra="ignore")
+
+    extend_pip_args: list[str] | None = None
     """Additional arguments that should be passed to ``pip install``.
 
     .. important::
@@ -326,8 +323,3 @@ class PythonHookArgs(AwsLambdaHookArgs):
 
     use_poetry: bool = True
     """Whether poetry should be used if determined appropriate."""
-
-    class Config:
-        """Model configuration."""
-
-        extra = Extra.ignore
diff --git a/runway/cfngin/hooks/awslambda/models/responses.py b/runway/cfngin/hooks/awslambda/models/responses.py
index 4d2003df7..d8e47a1e9 100644
--- a/runway/cfngin/hooks/awslambda/models/responses.py
+++ b/runway/cfngin/hooks/awslambda/models/responses.py
@@ -1,9 +1,10 @@
 """Response data models."""
 
-# ruff: noqa: UP006, UP035
-from typing import List, Optional
+from __future__ import annotations
 
-from pydantic import Extra
+from typing import Annotated
+
+from pydantic import ConfigDict, Field
 
 from runway.utils import BaseModel
 
@@ -19,31 +20,35 @@ class AwsLambdaHookDeployResponse(BaseModel):
 
     """
 
-    bucket_name: str
+    model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+    bucket_name: Annotated[str, Field(alias="S3Bucket")]
     """Name of the S3 Bucket where the deployment package is located. (alias ``S3Bucket``)"""
 
-    code_sha256: str
+    code_sha256: Annotated[str, Field(alias="CodeSha256")]
     """SHA256 of the deployment package.
     This can be used by CloudFormation as the value of ``AWS::Lambda::Version.CodeSha256``.
     (alias ``CodeSha256``)
 
     """
 
-    compatible_architectures: Optional[List[str]] = None
+    compatible_architectures: Annotated[
+        list[str] | None, Field(alias="CompatibleArchitectures")
+    ] = None
     """A list of compatible instruction set architectures.
     (https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html)
     (alias ``CompatibleArchitectures``)
 
     """
 
-    compatible_runtimes: Optional[List[str]] = None
+    compatible_runtimes: Annotated[list[str] | None, Field(alias="CompatibleRuntimes")] = None
     """A list of compatible function runtimes.
     Used for filtering with ``ListLayers`` and ``ListLayerVersions``.
     (alias ``CompatibleRuntimes``)
 
     """
 
-    license: Optional[str] = None
+    license: Annotated[str | None, Field(alias="License")] = None
     """The layer's software license (alias ``License``). Can be any of the following:
 
     - A SPDX license identifier (e.g. ``MIT``).
@@ -53,31 +58,15 @@ class AwsLambdaHookDeployResponse(BaseModel):
 
     """
 
-    object_key: str
+    object_key: Annotated[str, Field(alias="S3Key")]
     """Key (file path) of the deployment package S3 Object. (alias ``S3Key``)"""
 
-    object_version_id: Optional[str] = None
+    object_version_id: Annotated[str | None, Field(alias="S3ObjectVersion")] = None
     """The version ID of the deployment package S3 Object.
     This will only have a value if the S3 Bucket has versioning enabled.
     (alias ``S3ObjectVersion``)
 
     """
 
-    runtime: str
+    runtime: Annotated[str, Field(alias="Runtime")]
     """Runtime of the Lambda Function. (alias ``Runtime``)"""
-
-    class Config:
-        """Model configuration."""
-
-        allow_population_by_field_name = True
-        extra = Extra.forbid
-        fields = {
-            "bucket_name": {"alias": "S3Bucket"},
-            "code_sha256": {"alias": "CodeSha256"},
-            "compatible_architectures": {"alias": "CompatibleArchitectures"},
-            "compatible_runtimes": {"alias": "CompatibleRuntimes"},
-            "license": {"alias": "License"},
-            "object_key": {"alias": "S3Key"},
-            "object_version_id": {"alias": "S3ObjectVersion"},
-            "runtime": {"alias": "Runtime"},
-        }
diff --git a/runway/cfngin/hooks/awslambda/python_requirements/_docker.py b/runway/cfngin/hooks/awslambda/python_requirements/_docker.py
index a883e87c5..1995f7475 100644
--- a/runway/cfngin/hooks/awslambda/python_requirements/_docker.py
+++ b/runway/cfngin/hooks/awslambda/python_requirements/_docker.py
@@ -4,7 +4,7 @@
 
 import logging
 import re
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
 
 from docker.types.services import Mount
 
@@ -28,8 +28,8 @@ def __init__(
         self,
         project: PythonProject,
         *,
-        client: Optional[DockerClient] = None,
-        context: Optional[Union[CfnginContext, RunwayContext]] = None,
+        client: DockerClient | None = None,
+        context: CfnginContext | RunwayContext | None = None,
     ) -> None:
         """Instantiate class.
 
@@ -86,7 +86,7 @@ def install_commands(self) -> list[str]:
         return []
 
     @cached_property
-    def python_version(self) -> Optional[Version]:
+    def python_version(self) -> Version | None:
         """Version of Python installed in the docker container."""
         match = re.search(
             r"Python (?P<version>\S*)",
@@ -97,7 +97,7 @@ def python_version(self) -> Optional[Version]:
         return Version(match.group("version"))
 
     @cached_property
-    def runtime(self) -> Optional[str]:
+    def runtime(self) -> str | None:
         """AWS Lambda runtime determined from the docker container's Python version."""
         if not self.python_version:
             return None
diff --git a/runway/cfngin/hooks/awslambda/python_requirements/_project.py b/runway/cfngin/hooks/awslambda/python_requirements/_project.py
index 7b66dc6cf..a85c3fa4c 100644
--- a/runway/cfngin/hooks/awslambda/python_requirements/_project.py
+++ b/runway/cfngin/hooks/awslambda/python_requirements/_project.py
@@ -4,7 +4,7 @@
 
 import logging
 import shutil
-from typing import TYPE_CHECKING, ClassVar, Optional
+from typing import TYPE_CHECKING, ClassVar
 
 from .....compat import cached_property
 from .....dependency_managers import (
@@ -33,7 +33,7 @@ class PythonProject(Project[PythonHookArgs]):
     """Name of the default cache directory."""
 
     @cached_property
-    def docker(self) -> Optional[PythonDockerDependencyInstaller]:
+    def docker(self) -> PythonDockerDependencyInstaller | None:
         """Docker interface that can be used to build the project."""
         return PythonDockerDependencyInstaller.from_project(self)
 
@@ -72,7 +72,7 @@ def pip(self) -> Pip:
         return Pip(self.ctx, self.project_root)
 
     @cached_property
-    def pipenv(self) -> Optional[Pipenv]:
+    def pipenv(self) -> Pipenv | None:
         """Pipenv dependency manager.
 
         Return:
@@ -90,7 +90,7 @@ def pipenv(self) -> Optional[Pipenv]:
         raise PipenvNotFoundError
 
     @cached_property
-    def poetry(self) -> Optional[Poetry]:
+    def poetry(self) -> Poetry | None:
         """Poetry dependency manager.
 
         Return:
@@ -121,7 +121,7 @@ def project_type(self) -> Literal["pip", "pipenv", "poetry"]:
         return "pip"
 
     @cached_property
-    def requirements_txt(self) -> Optional[Path]:
+    def requirements_txt(self) -> Path | None:
         """Dependency file for the project."""
         if self.poetry:  # prioritize poetry
             return self.poetry.export(output=self.tmp_requirements_txt)
diff --git a/runway/cfngin/hooks/base.py b/runway/cfngin/hooks/base.py
index e515b16b4..668460ede 100644
--- a/runway/cfngin/hooks/base.py
+++ b/runway/cfngin/hooks/base.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, ClassVar, Dict, cast  # noqa: UP035
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from troposphere import Tags
 
@@ -28,7 +28,7 @@
 class HookArgsBaseModel(BaseModel):
     """Base model for hook args."""
 
-    tags: Dict[str, str] = {}  # noqa: UP006
+    tags: dict[str, str] = {}
 
 
 class Hook(CfnginHookProtocol):
@@ -46,10 +46,9 @@ class Hook(CfnginHookProtocol):
 
     """
 
-    ARGS_PARSER: ClassVar[type[HookArgsBaseModel]] = HookArgsBaseModel
+    ARGS_PARSER: ClassVar = HookArgsBaseModel
     """Class used to parse arguments passed to the hook."""
 
-    args: HookArgsBaseModel
     blueprint: Blueprint | None = None
     context: CfnginContext
     provider: Provider
@@ -67,7 +66,7 @@ def __init__(self, context: CfnginContext, provider: Provider, **kwargs: Any) ->
         """
         kwargs.setdefault("tags", {})
 
-        self.args = self.ARGS_PARSER.parse_obj(kwargs)
+        self.args = self.ARGS_PARSER.model_validate(kwargs)
         self.args.tags.update(context.tags)
         self.context = context
         self.provider = provider
@@ -82,7 +81,7 @@ def tags(self) -> Tags:
 
     def generate_stack(self, **kwargs: Any) -> Stack:
         """Create a CFNgin Stack object."""
-        definition = CfnginStackDefinitionModel.construct(
+        definition = CfnginStackDefinitionModel.model_construct(
             name=self.stack_name, tags=self.args.tags, **kwargs
         )
         stack = Stack(definition, self.context)
diff --git a/runway/cfngin/hooks/cleanup_s3.py b/runway/cfngin/hooks/cleanup_s3.py
index 860f58e1b..dba656331 100644
--- a/runway/cfngin/hooks/cleanup_s3.py
+++ b/runway/cfngin/hooks/cleanup_s3.py
@@ -24,7 +24,7 @@ class PurgeBucketHookArgs(BaseModel):
 
 def purge_bucket(context: CfnginContext, *__args: Any, **kwargs: Any) -> bool:
     """Delete objects in bucket."""
-    args = PurgeBucketHookArgs.parse_obj(kwargs)
+    args = PurgeBucketHookArgs.model_validate(kwargs)
     session = context.get_session()
     s3_resource = session.resource("s3")
     try:
diff --git a/runway/cfngin/hooks/cleanup_ssm.py b/runway/cfngin/hooks/cleanup_ssm.py
index 6b4f69cc2..b50fbc5b6 100644
--- a/runway/cfngin/hooks/cleanup_ssm.py
+++ b/runway/cfngin/hooks/cleanup_ssm.py
@@ -22,7 +22,7 @@ class DeleteParamHookArgs(BaseModel):
 
 def delete_param(context: CfnginContext, *__args: Any, **kwargs: Any) -> bool:
     """Delete SSM parameter."""
-    args = DeleteParamHookArgs.parse_obj(kwargs)
+    args = DeleteParamHookArgs.model_validate(kwargs)
 
     session = context.get_session()
     ssm_client = session.client("ssm")
diff --git a/runway/cfngin/hooks/command.py b/runway/cfngin/hooks/command.py
index 1cf202281..d30ee1eb1 100644
--- a/runway/cfngin/hooks/command.py
+++ b/runway/cfngin/hooks/command.py
@@ -1,9 +1,11 @@
 """Command hook."""
 
+from __future__ import annotations
+
 import logging
 import os
 import subprocess
-from typing import Any, Dict, List, Optional, Union  # noqa: UP035
+from typing import Any
 
 from typing_extensions import TypedDict
 
@@ -19,10 +21,10 @@ class RunCommandHookArgs(BaseModel):
     capture: bool = False
     """If enabled, capture the command's stdout and stderr, and return them in the hook result."""
 
-    command: Union[str, List[str]]  # noqa: UP006
+    command: str | list[str]
     """Command(s) to run."""
 
-    env: Optional[Dict[str, str]] = None  # noqa: UP006
+    env: dict[str, str] | None = None
     """Dictionary of environment variable overrides for the command context.
     Will be merged with the current environment.
 
@@ -43,7 +45,7 @@ class RunCommandHookArgs(BaseModel):
 
     """
 
-    stdin: Optional[str] = None
+    stdin: str | None = None
     """String to send to the stdin of the command. Implicitly disables ``interactive``."""
 
 
@@ -90,10 +92,10 @@ def run_command(*_args: Any, **kwargs: Any) -> RunCommandResponseTypeDef:  # noq
                     shell: true
 
     """
-    args = RunCommandHookArgs.parse_obj(kwargs)
+    args = RunCommandHookArgs.model_validate(kwargs)
 
     # remove parsed args from kwargs
-    for field in RunCommandHookArgs.__fields__:
+    for field in RunCommandHookArgs.model_fields:
         kwargs.pop(field, None)
 
     # remove unneeded args from kwargs
diff --git a/runway/cfngin/hooks/docker/_login.py b/runway/cfngin/hooks/docker/_login.py
index a88c4339b..b037eccd7 100644
--- a/runway/cfngin/hooks/docker/_login.py
+++ b/runway/cfngin/hooks/docker/_login.py
@@ -3,55 +3,64 @@
 from __future__ import annotations
 
 import logging
-from typing import Any, Optional
+from typing import TYPE_CHECKING, Annotated, Any
 
-from pydantic import Field, validator
+from pydantic import ConfigDict, Field, field_validator, model_validator
 
 from ....context import CfnginContext
 from ....utils import BaseModel
 from .data_models import ElasticContainerRegistry
 from .hook_data import DockerHookData
 
+if TYPE_CHECKING:
+    from pydantic import ValidationInfo
+
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
 class LoginArgs(BaseModel):
     """Args passed to the docker.login hook."""
 
-    _ctx: Optional[CfnginContext] = Field(default=None, alias="context", exclude=True)
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    ctx: Annotated[CfnginContext | None, Field(alias="context", exclude=True)] = None
 
-    dockercfg_path: Optional[str] = None
+    dockercfg_path: str | None = None
     """Path to a non-default Docker config file."""
 
-    ecr: Optional[ElasticContainerRegistry] = Field(default=None, exclude=True)
+    ecr: ElasticContainerRegistry | None = Field(default=None, exclude=True)
     """Information describing an ECR registry."""
 
-    email: Optional[str] = None
+    email: str | None = None
     """The email for the registry account."""
 
     password: str
     """The plaintext password for the registry account."""
 
-    registry: Optional[str] = None
+    registry: Annotated[str | None, Field(validate_default=True)] = None
     """URI of the registry to login to."""
 
     username: str = "AWS"
     """The registry username."""
 
-    @validator("ecr", pre=True, allow_reuse=True)  # type: ignore
-    def _set_ecr(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _set_ecr(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Set the value of ``ecr``."""
-        if v and isinstance(v, dict):
-            return ElasticContainerRegistry.parse_obj({"context": values.get("context"), **v})
-        return v
-
-    @validator("registry", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_registry(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+        if "ecr" in values and isinstance(values["ecr"], dict):
+            values["ecr"] = ElasticContainerRegistry.model_validate(
+                {"context": values.get("context"), **values["ecr"]}
+            )
+        return values
+
+    @field_validator("registry", mode="before")
+    @classmethod
+    def _set_registry(cls, v: Any, info: ValidationInfo) -> Any:
         """Set the value of ``registry``."""
         if v:
             return v
 
-        ecr: ElasticContainerRegistry | None = values.get("ecr")
+        ecr: ElasticContainerRegistry | None = info.data.get("ecr")
         if ecr:
             return ecr.fqn
 
@@ -66,8 +75,8 @@ def login(*, context: CfnginContext, **kwargs: Any) -> DockerHookData:
     kwargs are parsed by :class:`~runway.cfngin.hooks.docker.LoginArgs`.
 
     """
-    args = LoginArgs.parse_obj({"context": context, **kwargs})
+    args = LoginArgs.model_validate({"context": context, **kwargs})
     docker_hook_data = DockerHookData.from_cfngin_context(context)
-    docker_hook_data.client.login(**args.dict())
+    docker_hook_data.client.login(**args.model_dump())
     LOGGER.info("logged into %s", args.registry)
     return docker_hook_data.update_context(context)
diff --git a/runway/cfngin/hooks/docker/data_models.py b/runway/cfngin/hooks/docker/data_models.py
index 4e4acf339..6773b6b08 100644
--- a/runway/cfngin/hooks/docker/data_models.py
+++ b/runway/cfngin/hooks/docker/data_models.py
@@ -7,15 +7,17 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Optional, cast
+from typing import TYPE_CHECKING, Annotated, Any, ClassVar, cast
 
 from docker.models.images import Image
-from pydantic import Field, root_validator
+from pydantic import ConfigDict, Field, PrivateAttr, model_validator
 
-from ....context import CfnginContext
 from ....core.providers.aws import AccountDetails
 from ....utils import BaseModel, MutableMap
 
+if TYPE_CHECKING:
+    from ....context import CfnginContext
+
 ECR_REPO_FQN_TEMPLATE = "{aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/{repo_name}"
 
 
@@ -25,19 +27,18 @@ class ElasticContainerRegistry(BaseModel):
     PUBLIC_URI_TEMPLATE: ClassVar[str] = "public.ecr.aws/{registry_alias}/"
     URI_TEMPLATE: ClassVar[str] = "{aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/"
 
-    _ctx: Optional[CfnginContext] = Field(default=None, alias="context", exclude=True)
-    """CFNgin context."""
+    model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
 
-    account_id: Optional[str] = None
+    account_id: str | None = None
     """AWS account ID that owns the registry being logged into."""
 
-    alias: Optional[str] = None
+    alias: str | None = None
     """If it is a public repository, the alias of the repository."""
 
     public: bool = True
     """Whether the repository is public."""
 
-    region: Optional[str] = Field(default=None, alias="aws_region")
+    region: str | None = Field(default=None, alias="aws_region")
     """AWS region where the registry is located."""
 
     @property
@@ -47,38 +48,33 @@ def fqn(self) -> str:
             return self.PUBLIC_URI_TEMPLATE.format(registry_alias=self.alias)
         return self.URI_TEMPLATE.format(aws_account_id=self.account_id, aws_region=self.region)
 
-    @root_validator(allow_reuse=True, pre=True)  # type: ignore
-    def _set_defaults(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _set_defaults(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Set default values based on other values."""
         values.setdefault("public", bool(values.get("alias")))
 
         if not values["public"]:
             account_id = values.get("account_id")
             ctx: CfnginContext | None = values.get("context")
-            region = values.get("aws_region")
-            if not ctx and not (account_id or region):
+            aws_region = values.get("aws_region")
+            if not ctx and not (account_id or aws_region):
                 raise ValueError("context is required to resolve values")
             if ctx:
                 if not account_id:
                     values["account_id"] = AccountDetails(ctx).id
-                if not region:
+                if not aws_region:
                     values["aws_region"] = ctx.env.aws_region or "us-east-1"
-
         return values
 
 
 class DockerImage(BaseModel):
     """Wrapper for :class:`docker.models.images.Image`."""
 
-    image: Image
-    _repo: Optional[str] = None
-
-    class Config:
-        """Model configuration."""
+    model_config = ConfigDict(arbitrary_types_allowed=True)
 
-        arbitrary_types_allowed = True
-        fields = {"_repo": {"exclude": True}}
-        underscore_attrs_are_private = True
+    _repo: str | None = PrivateAttr(default=None)
+    image: Image
 
     @property
     def id(self) -> str:
@@ -108,11 +104,17 @@ def uri(self) -> MutableMap:
         """Return a mapping of tag to image URI."""
         return MutableMap(**{uri.split(":")[-1]: uri for uri in self.image.tags})
 
+    def __bool__(self) -> bool:
+        """Evaluate the boolean value of the object instance."""
+        return True
+
 
 class ElasticContainerRegistryRepository(BaseModel):
     """AWS Elastic Container Registry (ECR) Repository."""
 
-    name: str = Field(..., alias="repo_name")
+    model_config = ConfigDict(populate_by_name=True)
+
+    name: Annotated[str, Field(alias="repo_name")]
     """The name of the repository."""
 
     registry: ElasticContainerRegistry
diff --git a/runway/cfngin/hooks/docker/hook_data.py b/runway/cfngin/hooks/docker/hook_data.py
index 7c8e45908..73e6c8d66 100644
--- a/runway/cfngin/hooks/docker/hook_data.py
+++ b/runway/cfngin/hooks/docker/hook_data.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Optional, overload
+from typing import TYPE_CHECKING, overload
 
 from docker import DockerClient
 
@@ -17,7 +17,7 @@
 class DockerHookData(MutableMap):
     """Docker hook_data object."""
 
-    image: Optional[DockerImage] = None
+    image: DockerImage | None = None
 
     @cached_property
     def client(self) -> DockerClient:
@@ -30,7 +30,7 @@ def update_context(self, context: CfnginContext = ...) -> DockerHookData: ...
     @overload
     def update_context(self, context: None = ...) -> None: ...
 
-    def update_context(self, context: Optional[CfnginContext] = None) -> Optional[DockerHookData]:
+    def update_context(self, context: CfnginContext | None = None) -> DockerHookData | None:
         """Update context object with new the current DockerHookData."""
         if not context:
             return None
diff --git a/runway/cfngin/hooks/docker/image/_build.py b/runway/cfngin/hooks/docker/image/_build.py
index 4e63d600a..90dbc34bb 100644
--- a/runway/cfngin/hooks/docker/image/_build.py
+++ b/runway/cfngin/hooks/docker/image/_build.py
@@ -8,14 +8,9 @@
 
 import logging
 from pathlib import Path
-from typing import (  # noqa: UP035
-    Any,
-    Dict,
-    List,
-    Optional,
-)
+from typing import TYPE_CHECKING, Annotated, Any
 
-from pydantic import DirectoryPath, Field, validator
+from pydantic import ConfigDict, DirectoryPath, Field, field_validator
 
 from .....context import CfnginContext
 from .....utils import BaseModel
@@ -26,19 +21,22 @@
 )
 from ..hook_data import DockerHookData
 
+if TYPE_CHECKING:
+    from pydantic import ValidationInfo
+
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
 class DockerImageBuildApiOptions(BaseModel):
     """Options for controlling Docker."""
 
-    buildargs: Dict[str, Any] = {}  # noqa: UP006
+    buildargs: dict[str, Any] = {}
     """Dict of build-time variables that will be passed to Docker."""
 
     custom_context: bool = False
     """Whether to use custom context when providing a file object."""
 
-    extra_hosts: Dict[str, str] = {}  # noqa: UP006
+    extra_hosts: dict[str, str] = {}
     """Extra hosts to add to `/etc/hosts` in the build containers.
     Defined as a mapping of hostname to IP address.
 
@@ -47,16 +45,16 @@ class DockerImageBuildApiOptions(BaseModel):
     forcerm: bool = False
     """Always remove intermediate containers, even after unsuccessful builds."""
 
-    isolation: Optional[str] = None
+    isolation: str | None = None
     """Isolation technology used during build."""
 
-    network_mode: Optional[str] = None
+    network_mode: str | None = None
     """Network mode for the run commands during build."""
 
     nocache: bool = False
     """Whether to use cache."""
 
-    platform: Optional[str] = None
+    platform: str | None = None
     """Set platform if server is multi-platform capable.
     Uses format ``os[/arch[/variant]]``.
 
@@ -71,13 +69,13 @@ class DockerImageBuildApiOptions(BaseModel):
     squash: bool = False
     """Whether to squash the resulting image layers into a single layer."""
 
-    tag: Optional[str] = None
+    tag: str | None = None
     """Optional name and tag to apply to the base image when it is built."""
 
-    target: Optional[str] = None
+    target: str | None = None
     """Name of the build-stage to build in a multi-stage Dockerfile."""
 
-    timeout: Optional[int] = None
+    timeout: int | None = None
     """HTTP timeout."""
 
     use_config_proxy: bool = False
@@ -92,9 +90,11 @@ class DockerImageBuildApiOptions(BaseModel):
 class ImageBuildArgs(BaseModel):
     """Args passed to image.build."""
 
-    _ctx: Optional[CfnginContext] = Field(default=None, alias="context", export=False)
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    ctx: Annotated[CfnginContext | None, Field(alias="context", exclude=True)] = None
 
-    ecr_repo: Optional[ElasticContainerRegistryRepository] = None  # depends on _ctx
+    ecr_repo: ElasticContainerRegistryRepository | None = None  # depends on ctx
     """AWS Elastic Container Registry repository information.
     Providing this will automatically construct the repo URI.
     If provided, do not provide ``repo``.
@@ -110,23 +110,26 @@ class ImageBuildArgs(BaseModel):
     dockerfile: str = "Dockerfile"  # depends on path for validation
     """Path within the build context to the Dockerfile."""
 
-    repo: Optional[str] = None  # depends on ecr_repo
+    repo: Annotated[str | None, Field(validate_default=True)] = None  # depends on ecr_repo & image
     """URI of a non Docker Hub repository where the image will be stored."""
 
-    docker: DockerImageBuildApiOptions = DockerImageBuildApiOptions()  # depends on repo
+    docker: Annotated[  # depends on repo
+        DockerImageBuildApiOptions, Field(validate_default=True)
+    ] = DockerImageBuildApiOptions()
     """Options for ``docker image build``."""
 
-    tags: List[str] = ["latest"]  # noqa: UP006
+    tags: Annotated[list[str], Field(validate_default=True)] = ["latest"]
     """List of tags to apply to the image."""
 
-    @validator("docker", pre=True, always=True, allow_reuse=True)  # type: ignore
+    @field_validator("docker", mode="before")
+    @classmethod
     def _set_docker(
-        cls,  # noqa: N805
+        cls,
         v: dict[str, Any] | DockerImageBuildApiOptions | Any,
-        values: dict[str, Any],
+        info: ValidationInfo,
     ) -> Any:
         """Set the value of ``docker``."""
-        repo = values["repo"]
+        repo = info.data["repo"]
         if repo:
             if isinstance(v, dict):
                 v.setdefault("tag", repo)
@@ -134,41 +137,44 @@ def _set_docker(
                 v.tag = repo
         return v
 
-    @validator("ecr_repo", pre=True, allow_reuse=True)  # type: ignore
-    def _set_ecr_repo(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+    @field_validator("ecr_repo", mode="before")
+    @classmethod
+    def _set_ecr_repo(cls, v: Any, info: ValidationInfo) -> Any:
         """Set the value of ``ecr_repo``."""
         if v and isinstance(v, dict):
-            return ElasticContainerRegistryRepository.parse_obj(
+            return ElasticContainerRegistryRepository.model_validate(
                 {
                     "repo_name": v.get("repo_name"),
-                    "registry": ElasticContainerRegistry.parse_obj(
+                    "registry": ElasticContainerRegistry.model_validate(
                         {
                             "account_id": v.get("account_id"),
                             "alias": v.get("registry_alias"),
                             "aws_region": v.get("aws_region"),
-                            "context": values.get("context"),
+                            "context": info.data.get("context"),
                         }
                     ),
                 }
             )
         return v
 
-    @validator("repo", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_repo(cls, v: str | None, values: dict[str, Any]) -> str | None:  # noqa: N805
+    @field_validator("repo", mode="before")
+    @classmethod
+    def _set_repo(cls, v: str | None, info: ValidationInfo) -> str | None:
         """Set the value of ``repo``."""
         if v:
             return v
 
-        ecr_repo: ElasticContainerRegistryRepository | None = values.get("ecr_repo")
+        ecr_repo: ElasticContainerRegistryRepository | None = info.data.get("ecr_repo")
         if ecr_repo:
             return ecr_repo.fqn
 
         return None
 
-    @validator("dockerfile", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _validate_dockerfile(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+    @field_validator("dockerfile", mode="before")
+    @classmethod
+    def _validate_dockerfile(cls, v: Any, info: ValidationInfo) -> Any:
         """Validate ``dockerfile``."""
-        path: Path = values["path"]
+        path: Path = info.data["path"]
         dockerfile = path / v
         if not dockerfile.is_file():
             raise ValueError(f"Dockerfile does not exist at path provided: {dockerfile}")
@@ -183,9 +189,11 @@ def build(*, context: CfnginContext, **kwargs: Any) -> DockerHookData:
     kwargs are parsed by :class:`~runway.cfngin.hooks.docker.image.ImageBuildArgs`.
 
     """
-    args = ImageBuildArgs.parse_obj({"context": context, **kwargs})
+    args = ImageBuildArgs.model_validate({"context": context, **kwargs})
     docker_hook_data = DockerHookData.from_cfngin_context(context)
-    image, logs = docker_hook_data.client.images.build(path=str(args.path), **args.docker.dict())
+    image, logs = docker_hook_data.client.images.build(
+        path=str(args.path), **args.docker.model_dump()
+    )
     for msg in logs:  # iterate through JSON log messages
         if "stream" in msg:  # log if they contain a message
             LOGGER.info(msg["stream"].strip())  # remove any new line characters
diff --git a/runway/cfngin/hooks/docker/image/_push.py b/runway/cfngin/hooks/docker/image/_push.py
index 173daed6d..f4da40288 100644
--- a/runway/cfngin/hooks/docker/image/_push.py
+++ b/runway/cfngin/hooks/docker/image/_push.py
@@ -7,9 +7,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any, List, Optional  # noqa: UP035
+from typing import TYPE_CHECKING, Annotated, Any
 
-from pydantic import Field, validator
+from pydantic import ConfigDict, Field, field_validator
 
 from .....context import CfnginContext
 from .....utils import BaseModel
@@ -20,15 +20,20 @@
 )
 from ..hook_data import DockerHookData
 
+if TYPE_CHECKING:
+    from pydantic import ValidationInfo
+
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
 class ImagePushArgs(BaseModel):
     """Args passed to image.push."""
 
-    _ctx: Optional[CfnginContext] = Field(default=None, alias="context", export=False)
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    ctx: Annotated[CfnginContext | None, Field(alias="context", exclude=True)] = None
 
-    ecr_repo: Optional[ElasticContainerRegistryRepository] = None  # depends on _ctx
+    ecr_repo: ElasticContainerRegistryRepository | None = None  # depends on ctx
     """AWS Elastic Container Registry repository information.
     Providing this will automatically construct the repo URI.
     If provided, do not provide ``repo``.
@@ -38,57 +43,60 @@ class ImagePushArgs(BaseModel):
 
     """
 
-    image: Optional[DockerImage] = None
+    image: DockerImage | None = None
     """Image to push."""
 
-    repo: Optional[str] = None  # depends on ecr_repo & image
+    repo: Annotated[str | None, Field(validate_default=True)] = None  # depends on ecr_repo & image
     """URI of a non Docker Hub repository where the image will be stored."""
 
-    tags: List[str] = []  # depends on image  # noqa: UP006
+    tags: Annotated[list[str], Field(validate_default=True)] = []  # depends on image
     """List of tags to push."""
 
-    @validator("ecr_repo", pre=True, allow_reuse=True)  # type: ignore
-    def _set_ecr_repo(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+    @field_validator("ecr_repo", mode="before")
+    @classmethod
+    def _set_ecr_repo(cls, v: Any, info: ValidationInfo) -> Any:
         """Set the value of ``ecr_repo``."""
         if v and isinstance(v, dict):
-            return ElasticContainerRegistryRepository.parse_obj(
+            return ElasticContainerRegistryRepository.model_validate(
                 {
                     "repo_name": v.get("repo_name"),
-                    "registry": ElasticContainerRegistry.parse_obj(
+                    "registry": ElasticContainerRegistry.model_validate(
                         {
                             "account_id": v.get("account_id"),
                             "alias": v.get("registry_alias"),
                             "aws_region": v.get("aws_region"),
-                            "context": values.get("context"),
+                            "context": info.data.get("context"),
                         }
                     ),
                 }
             )
         return v
 
-    @validator("repo", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_repo(cls, v: str | None, values: dict[str, Any]) -> str | None:  # noqa: N805
+    @field_validator("repo", mode="before")
+    @classmethod
+    def _set_repo(cls, v: str | None, info: ValidationInfo) -> str | None:
         """Set the value of ``repo``."""
         if v:
             return v
 
-        image: DockerImage | None = values.get("image")
+        image: DockerImage | None = info.data.get("image")
         if image:
             return image.repo
 
-        ecr_repo: ElasticContainerRegistryRepository | None = values.get("ecr_repo")
+        ecr_repo: ElasticContainerRegistryRepository | None = info.data.get("ecr_repo")
         if ecr_repo:
             return ecr_repo.fqn
 
         return None
 
-    @validator("tags", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_tags(cls, v: list[str], values: dict[str, Any]) -> list[str]:  # noqa: N805
+    @field_validator("tags", mode="before")
+    @classmethod
+    def _set_tags(cls, v: list[str], info: ValidationInfo) -> list[str]:
         """Set the value of ``tags``."""
         if v:
             return v
 
-        image: DockerImage | None = values.get("image")
+        image: DockerImage | None = info.data.get("image")
         if image:
             return image.tags
 
@@ -103,7 +111,7 @@ def push(*, context: CfnginContext, **kwargs: Any) -> DockerHookData:
     kwargs are parsed by :class:`~runway.cfngin.hooks.docker.image.ImagePushArgs`.
 
     """
-    args = ImagePushArgs.parse_obj({"context": context, **kwargs})
+    args = ImagePushArgs.model_validate({"context": context, **kwargs})
     docker_hook_data = DockerHookData.from_cfngin_context(context)
     LOGGER.info("pushing image %s...", args.repo)
     for tag in args.tags:
diff --git a/runway/cfngin/hooks/docker/image/_remove.py b/runway/cfngin/hooks/docker/image/_remove.py
index 5aabd4c4a..108df5cc7 100644
--- a/runway/cfngin/hooks/docker/image/_remove.py
+++ b/runway/cfngin/hooks/docker/image/_remove.py
@@ -7,10 +7,10 @@
 from __future__ import annotations
 
 import logging
-from typing import Any, List, Optional  # noqa: UP035
+from typing import TYPE_CHECKING, Annotated, Any
 
 from docker.errors import ImageNotFound
-from pydantic import Field, validator
+from pydantic import ConfigDict, Field, field_validator
 
 from .....context import CfnginContext
 from .....utils import BaseModel
@@ -21,15 +21,20 @@
 )
 from ..hook_data import DockerHookData
 
+if TYPE_CHECKING:
+    from pydantic import ValidationInfo
+
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
 class ImageRemoveArgs(BaseModel):
     """Args passed to image.remove."""
 
-    _ctx: Optional[CfnginContext] = Field(default=None, alias="context", export=False)
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    ctx: Annotated[CfnginContext | None, Field(alias="context", exclude=True)] = None
 
-    ecr_repo: Optional[ElasticContainerRegistryRepository] = None  # depends on _ctx
+    ecr_repo: ElasticContainerRegistryRepository | None = None  # depends on ctx
     """AWS Elastic Container Registry repository information.
     Providing this will automatically construct the repo URI.
     If provided, do not provide ``repo``.
@@ -42,60 +47,63 @@ class ImageRemoveArgs(BaseModel):
     force: bool = False
     """Whether to force the removal of the image."""
 
-    image: Optional[DockerImage] = None
+    image: DockerImage | None = None
     """Image to push."""
 
     noprune: bool = False
     """Whether to delete untagged parents."""
 
-    repo: Optional[str] = None  # depends on ecr_repo & image
+    repo: Annotated[str | None, Field(validate_default=True)] = None  # depends on ecr_repo & image
     """URI of a non Docker Hub repository where the image will be stored."""
 
-    tags: List[str] = []  # depends on image  # noqa: UP006
-    """List of tags to remove."""
+    tags: Annotated[list[str], Field(validate_default=True)] = []
+    """List of tags to apply to the image."""
 
-    @validator("ecr_repo", pre=True, allow_reuse=True)  # type: ignore
-    def _set_ecr_repo(cls, v: Any, values: dict[str, Any]) -> Any:  # noqa: N805
+    @field_validator("ecr_repo", mode="before")
+    @classmethod
+    def _set_ecr_repo(cls, v: Any, info: ValidationInfo) -> Any:
         """Set the value of ``ecr_repo``."""
         if v and isinstance(v, dict):
-            return ElasticContainerRegistryRepository.parse_obj(
+            return ElasticContainerRegistryRepository.model_validate(
                 {
                     "repo_name": v.get("repo_name"),
-                    "registry": ElasticContainerRegistry.parse_obj(
+                    "registry": ElasticContainerRegistry.model_validate(
                         {
                             "account_id": v.get("account_id"),
                             "alias": v.get("registry_alias"),
                             "aws_region": v.get("aws_region"),
-                            "context": values.get("context"),
+                            "context": info.data.get("context"),
                         }
                     ),
                 }
             )
         return v
 
-    @validator("repo", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_repo(cls, v: str | None, values: dict[str, Any]) -> str | None:  # noqa: N805
+    @field_validator("repo", mode="before")
+    @classmethod
+    def _set_repo(cls, v: str | None, info: ValidationInfo) -> str | None:
         """Set the value of ``repo``."""
         if v:
             return v
 
-        image: DockerImage | None = values.get("image")
+        image: DockerImage | None = info.data.get("image")
         if image:
             return image.repo
 
-        ecr_repo: ElasticContainerRegistryRepository | None = values.get("ecr_repo")
+        ecr_repo: ElasticContainerRegistryRepository | None = info.data.get("ecr_repo")
         if ecr_repo:
             return ecr_repo.fqn
 
         return None
 
-    @validator("tags", pre=True, always=True, allow_reuse=True)  # type: ignore
-    def _set_tags(cls, v: list[str], values: dict[str, Any]) -> list[str]:  # noqa: N805
+    @field_validator("tags", mode="before")
+    @classmethod
+    def _set_tags(cls, v: list[str], info: ValidationInfo) -> list[str]:
         """Set the value of ``tags``."""
         if v:
             return v
 
-        image: DockerImage | None = values.get("image")
+        image: DockerImage | None = info.data.get("image")
         if image:
             return image.tags
 
@@ -110,7 +118,7 @@ def remove(*, context: CfnginContext, **kwargs: Any) -> DockerHookData:
     kwargs are parsed by :class:`~runway.cfngin.hooks.docker.image.ImageRemoveArgs`.
 
     """
-    args = ImageRemoveArgs.parse_obj({"context": context, **kwargs})
+    args = ImageRemoveArgs.model_validate({"context": context, **kwargs})
     docker_hook_data = DockerHookData.from_cfngin_context(context)
     LOGGER.info("removing local image %s...", args.repo)
     for tag in args.tags:
diff --git a/runway/cfngin/hooks/ecr/_purge_repository.py b/runway/cfngin/hooks/ecr/_purge_repository.py
index 013eb543c..f117f5b7d 100644
--- a/runway/cfngin/hooks/ecr/_purge_repository.py
+++ b/runway/cfngin/hooks/ecr/_purge_repository.py
@@ -56,7 +56,8 @@ def list_ecr_images(client: ECRClient, repository_name: str) -> list[ImageIdenti
             image_ids.extend(response["imageIds"])
         return [
             {"imageDigest": digest}
-            for digest in {image["imageDigest"] for image in image_ids if image.get("imageDigest")}
+            for digest in {image.get("imageDigest") for image in image_ids}
+            if digest
         ]
     except client.exceptions.RepositoryNotFoundException:
         LOGGER.info("repository %s does not exist", repository_name)
@@ -71,7 +72,7 @@ def purge_repository(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     client = context.get_session().client("ecr")
     image_ids = list_ecr_images(client, repository_name=args.repository_name)
     if not image_ids:
diff --git a/runway/cfngin/hooks/ecs.py b/runway/cfngin/hooks/ecs.py
index 69a62e3b1..0bcb03938 100644
--- a/runway/cfngin/hooks/ecs.py
+++ b/runway/cfngin/hooks/ecs.py
@@ -3,9 +3,9 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, List  # noqa: UP035
+from typing import TYPE_CHECKING, Any
 
-from pydantic import validator
+from pydantic import field_validator
 from typing_extensions import TypedDict
 
 from ...utils import BaseModel
@@ -21,11 +21,12 @@
 class CreateClustersHookArgs(BaseModel):
     """Hook arguments for ``create_clusters``."""
 
-    clusters: List[str]  # noqa: UP006
+    clusters: list[str]
     """List of cluster names to create."""
 
-    @validator("clusters", allow_reuse=True, pre=True)  # type: ignore
-    def _convert_clusters(cls, v: list[str] | str) -> list[str]:  # noqa: N805
+    @field_validator("clusters", mode="before")
+    @classmethod
+    def _convert_clusters(cls, v: list[str] | str) -> list[str]:
         """Convert value of ``clusters`` from str to list."""
         if isinstance(v, str):
             return [v]
@@ -48,7 +49,7 @@ def create_clusters(
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = CreateClustersHookArgs.parse_obj(kwargs)
+    args = CreateClustersHookArgs.model_validate(kwargs)
     ecs_client = context.get_session().client("ecs")
 
     cluster_info: dict[str, Any] = {}
diff --git a/runway/cfngin/hooks/iam.py b/runway/cfngin/hooks/iam.py
index edf9a6ab4..143336234 100644
--- a/runway/cfngin/hooks/iam.py
+++ b/runway/cfngin/hooks/iam.py
@@ -4,7 +4,7 @@
 
 import copy
 import logging
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 from awacs import ecs
 from awacs.aws import Allow, Policy, Statement
@@ -56,13 +56,13 @@ class EnsureServerCertExistsHookArgs(BaseModel):
     cert_name: str
     """Name of the certificate that should exist."""
 
-    path_to_certificate: Optional[str] = None
+    path_to_certificate: str | None = None
     """Path to certificate file."""
 
-    path_to_chain: Optional[str] = None
+    path_to_chain: str | None = None
     """Path to chain file."""
 
-    path_to_private_key: Optional[str] = None
+    path_to_private_key: str | None = None
     """Path to private key file."""
 
     prompt: bool = True
@@ -79,7 +79,7 @@ def create_ecs_service_role(context: CfnginContext, *_args: Any, **kwargs: Any)
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = CreateEcsServiceRoleHookArgs.parse_obj(kwargs)
+    args = CreateEcsServiceRoleHookArgs.model_validate(kwargs)
     client = context.get_session().client("iam")
 
     try:
@@ -179,7 +179,7 @@ def ensure_server_cert_exists(context: CfnginContext, *_args: Any, **kwargs: Any
         Dict containing ``status``, ``cert_name``, and ``cert_arn``.
 
     """
-    args = EnsureServerCertExistsHookArgs.parse_obj(kwargs)
+    args = EnsureServerCertExistsHookArgs.model_validate(kwargs)
     client = context.get_session().client("iam")
     status = "unknown"
     try:
@@ -193,7 +193,7 @@ def ensure_server_cert_exists(context: CfnginContext, *_args: Any, **kwargs: Any
             if upload != "yes":
                 return {}
 
-        parameters = _get_cert_contents(args.dict())
+        parameters = _get_cert_contents(args.model_dump())
         if not parameters:
             return {}
         response = client.upload_server_certificate(**parameters)
diff --git a/runway/cfngin/hooks/keypair.py b/runway/cfngin/hooks/keypair.py
index 88bd1f176..d80c22787 100644
--- a/runway/cfngin/hooks/keypair.py
+++ b/runway/cfngin/hooks/keypair.py
@@ -5,7 +5,7 @@
 import logging
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 
 from botocore.exceptions import ClientError
 from typing_extensions import Literal, TypedDict
@@ -31,19 +31,19 @@ class EnsureKeypairExistsHookArgs(BaseModel):
     keypair: str
     """Name of the key pair to ensure exists."""
 
-    public_key_path: Optional[str] = None
+    public_key_path: str | None = None
     """Path to a public key file to be imported instead of generating a new key.
     Incompatible with the SSM options, as the private key will not be available for storing.
 
     """
 
-    ssm_key_id: Optional[str] = None
+    ssm_key_id: str | None = None
     """ID of a KMS key to encrypt the SSM parameter with.
     If omitted, the default key will be used.
 
     """
 
-    ssm_parameter_name: Optional[str] = None
+    ssm_parameter_name: str | None = None
     """Path to an SSM store parameter to receive the generated private key
     instead of importing it or storing it locally.
 
@@ -59,7 +59,7 @@ class KeyPairInfo(TypedDict, total=False):
     status: Literal["created", "exists", "imported"]
 
 
-def get_existing_key_pair(ec2: EC2Client, keypair_name: str) -> Optional[KeyPairInfo]:
+def get_existing_key_pair(ec2: EC2Client, keypair_name: str) -> KeyPairInfo | None:
     """Get existing keypair."""
     resp = ec2.describe_key_pairs()
     keypair = next(
@@ -100,7 +100,7 @@ def import_key_pair(
     return keypair
 
 
-def read_public_key_file(path: Path) -> Optional[bytes]:
+def read_public_key_file(path: Path) -> bytes | None:
     """Read public key file."""
     try:
         data = path.read_bytes()
@@ -117,7 +117,7 @@ def read_public_key_file(path: Path) -> Optional[bytes]:
 
 def create_key_pair_from_public_key_file(
     ec2: EC2Client, keypair_name: str, public_key_path: Path
-) -> Optional[KeyPairInfo]:
+) -> KeyPairInfo | None:
     """Create keypair from public key file."""
     public_key_data = read_public_key_file(public_key_path)
     if not public_key_data:
@@ -136,8 +136,8 @@ def create_key_pair_in_ssm(
     ssm: SSMClient,
     keypair_name: str,
     parameter_name: str,
-    kms_key_id: Optional[str] = None,
-) -> Optional[KeyPairInfo]:
+    kms_key_id: str | None = None,
+) -> KeyPairInfo | None:
     """Create keypair in SSM."""
     keypair = create_key_pair(ec2, keypair_name)
     try:
@@ -191,9 +191,7 @@ def create_key_pair(ec2: EC2Client, keypair_name: str) -> KeyPairTypeDef:
     return keypair
 
 
-def create_key_pair_local(
-    ec2: EC2Client, keypair_name: str, dest_dir: Path
-) -> Optional[KeyPairInfo]:
+def create_key_pair_local(ec2: EC2Client, keypair_name: str, dest_dir: Path) -> KeyPairInfo | None:
     """Create local keypair."""
     dest_dir = dest_dir.resolve()
     if not dest_dir.is_dir():
@@ -219,7 +217,7 @@ def create_key_pair_local(
 
 def interactive_prompt(
     keypair_name: str,
-) -> tuple[Optional[Literal["create", "import"]], Optional[str]]:
+) -> tuple[Literal["create", "import"] | None, str | None]:
     """Interactive prompt."""
     if not sys.stdin.isatty():
         return None, None
@@ -252,7 +250,7 @@ def ensure_keypair_exists(context: CfnginContext, *__args: Any, **kwargs: Any) -
     If the key doesn't exist, upload it.
 
     """
-    args = EnsureKeypairExistsHookArgs.parse_obj(kwargs)
+    args = EnsureKeypairExistsHookArgs.model_validate(kwargs)
 
     if args.public_key_path and args.ssm_parameter_name:
         LOGGER.error("public_key_path and ssm_parameter_name cannot be specified at the same time")
diff --git a/runway/cfngin/hooks/protocols.py b/runway/cfngin/hooks/protocols.py
index 9eb3bc43c..9e0ddbcdf 100644
--- a/runway/cfngin/hooks/protocols.py
+++ b/runway/cfngin/hooks/protocols.py
@@ -8,7 +8,7 @@
 from __future__ import annotations
 
 from abc import abstractmethod
-from typing import TYPE_CHECKING, Any, TypeVar, overload
+from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
 
 from typing_extensions import Protocol, runtime_checkable
 
@@ -75,8 +75,8 @@ class CfnginHookProtocol(Protocol):
 
     """
 
-    args: CfnginHookArgsProtocol
-    """Arguments passed to the hook and parsed into an object."""
+    ARGS_PARSER: ClassVar
+    """Class used to parse arguments passed to the hook."""
 
     @abstractmethod
     def __init__(self, context: CfnginContext, **_kwargs: Any) -> None:
diff --git a/runway/cfngin/hooks/route53.py b/runway/cfngin/hooks/route53.py
index 6cffb4a36..288f2c491 100644
--- a/runway/cfngin/hooks/route53.py
+++ b/runway/cfngin/hooks/route53.py
@@ -32,7 +32,7 @@ def create_domain(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict[st
         Dict containing ``domain`` and ``zone_id``.
 
     """
-    args = CreateDomainHookArgs.parse_obj(kwargs)
+    args = CreateDomainHookArgs.model_validate(kwargs)
     client = context.get_session().client("route53")
     zone_id = create_route53_zone(client, args.domain)
     return {"domain": args.domain, "zone_id": zone_id}
diff --git a/runway/cfngin/hooks/ssm/parameter.py b/runway/cfngin/hooks/ssm/parameter.py
index 56ad26242..067cf1eb8 100644
--- a/runway/cfngin/hooks/ssm/parameter.py
+++ b/runway/cfngin/hooks/ssm/parameter.py
@@ -4,9 +4,9 @@
 
 import json
 import logging
-from typing import TYPE_CHECKING, Any, List, Optional, cast  # noqa: UP035
+from typing import TYPE_CHECKING, Annotated, Any, ClassVar, cast
 
-from pydantic import Extra, validator
+from pydantic import ConfigDict, Field, field_validator
 from typing_extensions import Literal, TypedDict
 
 from ....compat import cached_property
@@ -61,64 +61,95 @@ class ArgsDataModel(BaseModel):
 
     """
 
-    allowed_pattern: Optional[str] = None
-    data_type: Optional[Literal["aws:ec2:image", "text"]] = None
-    description: Optional[str] = None
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    allowed_pattern: Annotated[str | None, Field(alias="AllowedPattern")] = None
+    """A regular expression used to validate the parameter value."""
+
+    data_type: Annotated[
+        Literal["aws:ec2:image", "text"] | None,
+        Field(alias="DataType"),
+    ] = None
+    """The data type for a String parameter.
+
+    Supported data types include plain text and Amazon Machine Image IDs.
+
+    """
+
+    description: Annotated[str | None, Field(alias="Description")] = None
+    """Information about the parameter."""
+
     force: bool = False
-    key_id: Optional[str] = None
-    name: str
-    overwrite: bool = True
-    policies: Optional[str] = None
-    tags: Optional[List[TagDataModel]] = None  # noqa: UP006
-    tier: ParameterTierType = "Standard"
-    type: Literal["String", "StringList", "SecureString"]
-    value: Optional[str] = None
-
-    class Config:
-        """Model configuration."""
-
-        allow_population_by_field_name = True
-        extra = Extra.ignore
-        fields = {
-            "allowed_pattern": {"alias": "AllowedPattern"},
-            "data_type": {"alias": "DataType"},
-            "description": {"alias": "Description"},
-            "key_id": {"alias": "KeyId"},
-            "name": {"alias": "Name"},
-            "overwrite": {"alias": "Overwrite"},
-            "policies": {"alias": "Policies"},
-            "tags": {"alias": "Tags"},
-            "tier": {"alias": "Tier"},
-            "type": {"alias": "Type"},
-            "value": {"alias": "Value"},
-        }
-
-    @validator("policies", allow_reuse=True, pre=True)  # type: ignore
-    def _convert_policies(cls, v: list[dict[str, Any]] | str | Any) -> str:  # noqa: N805
+    """Skip checking the current value of the parameter, just put it.
+
+    Can be used alongside ``overwrite`` to always update a parameter.
+
+    """
+
+    key_id: Annotated[str | None, Field(alias="KeyId")] = None
+    """The KMS Key ID that you want to use to encrypt a parameter.
+
+    Either the default AWS Key Management Service (AWS KMS) key automatically
+    assigned to your AWS account or a custom key.
+    Required for parameters that use the ``SecureString`` data type.
+
+    """
+
+    name: Annotated[str, Field(alias="Name")]
+    """The fully qualified name of the parameter that you want to add to the system."""
+
+    overwrite: Annotated[bool, Field(alias="Overwrite")] = True
+    """Allow overwriting an existing parameter."""
+
+    policies: Annotated[str | None, Field(alias="Policies")] = None
+    """One or more policies to apply to a parameter. This field takes a JSON array."""
+
+    tags: Annotated[list[TagDataModel] | None, Field(alias="Tags")] = None
+    """Optional metadata that you assign to a resource."""
+
+    tier: Annotated[ParameterTierType, Field(alias="Tier")] = "Standard"
+    """The parameter tier to assign to a parameter."""
+
+    type: Annotated[Literal["String", "StringList", "SecureString"], Field(alias="Type")]
+    """The type of parameter."""
+
+    value: Annotated[str | None, Field(alias="Value")] = None
+    """The parameter value that you want to add to the system.
+
+    Standard parameters have a value limit of 4 KB.
+    Advanced parameters have a value limit of 8 KB.
+
+    """
+
+    @field_validator("policies", mode="before")
+    @classmethod
+    def _convert_policies(cls, v: list[dict[str, Any]] | str | Any) -> str:
         """Convert policies to acceptable value."""
         if isinstance(v, str):
             return v
         if isinstance(v, list):
             return json.dumps(v, cls=JsonEncoder)
-        raise TypeError(f"unexpected type {type(v)}; permitted: list[dict[str, Any]] | str | None")
+        raise ValueError(f"unexpected type {type(v)}; permitted: list[dict[str, Any]] | str | None")
 
-    @validator("tags", allow_reuse=True, pre=True)  # type: ignore
-    def _convert_tags(
-        cls, v: dict[str, str] | list[dict[str, str]] | Any  # noqa: N805
-    ) -> list[dict[str, str]]:
+    @field_validator("tags", mode="before")
+    @classmethod
+    def _convert_tags(cls, v: dict[str, str] | list[dict[str, str]] | Any) -> list[dict[str, str]]:
         """Convert tags to acceptable value."""
         if isinstance(v, list):
             return v
         if isinstance(v, dict):
             return [{"Key": k, "Value": v} for k, v in v.items()]
-        raise TypeError(
-            f"unexpected type {type(v)}; permitted: dict[str, str] | list[dict[str, str] | None"
+        raise ValueError(
+            f"unexpected type {type(v)}; permitted: dict[str, str] | list[dict[str, str]] | none"
         )
 
 
 class _Parameter(CfnginHookProtocol):
     """AWS SSM Parameter Store Parameter."""
 
+    ARGS_PARSER: ClassVar = ArgsDataModel
+    """Class used to parse arguments passed to the hook."""
+
     args: ArgsDataModel
 
     def __init__(
@@ -139,7 +170,7 @@ def __init__(
             **kwargs: Arbitrary keyword arguments.
 
         """
-        self.args = ArgsDataModel.parse_obj({"name": name, "type": type, **kwargs})
+        self.args = ArgsDataModel.model_validate({"name": name, "type": type, **kwargs})
         self.ctx = context
 
     @cached_property
@@ -212,7 +243,9 @@ def put(self) -> _PutParameterResultTypeDef:
         if current_param.get("Value") != self.args.value:
             try:
                 result = self.client.put_parameter(
-                    **self.args.dict(by_alias=True, exclude_none=True, exclude={"force", "tags"})
+                    **self.args.model_dump(
+                        by_alias=True, exclude_none=True, exclude={"force", "tags"}
+                    )
                 )
             except self.client.exceptions.ParameterAlreadyExists:
                 LOGGER.warning(
@@ -254,7 +287,7 @@ def update_tags(self) -> None:
 
             if self.args.tags:
                 tags_to_add = [
-                    cast("TagTypeDef", tag.dict(by_alias=True)) for tag in self.args.tags
+                    cast("TagTypeDef", tag.model_dump(by_alias=True)) for tag in self.args.tags
                 ]
                 self.client.add_tags_to_resource(
                     ResourceId=self.args.name,
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/callback_url_retriever.py b/runway/cfngin/hooks/staticsite/auth_at_edge/callback_url_retriever.py
index 8ecdd37e5..d815e3257 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/callback_url_retriever.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/callback_url_retriever.py
@@ -8,7 +8,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 
 from ...base import HookArgsBaseModel
 
@@ -24,7 +24,7 @@ class HookArgs(HookArgsBaseModel):
     stack_name: str
     """The name of the stack to check against."""
 
-    user_pool_arn: Optional[str] = None
+    user_pool_arn: str | None = None
     """The ARN of the User Pool to check for a client."""
 
 
@@ -45,7 +45,7 @@ def get(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict[str, Any]:
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     session = context.get_session()
     cloudformation_client = session.client("cloudformation")
     cognito_client = session.client("cognito-idp")
@@ -55,22 +55,29 @@ def get(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict[str, Any]:
         # Return the current stack if one exists
         stack_desc = cloudformation_client.describe_stacks(StackName=args.stack_name)
         # Get the client_id from the outputs
-        outputs = stack_desc["Stacks"][0]["Outputs"]
+        outputs = stack_desc["Stacks"][0].get("Outputs", [])
 
         if args.user_pool_arn:
             user_pool_id = args.user_pool_arn.split("/")[-1:][0]
         else:
             user_pool_id = next(
-                o["OutputValue"] for o in outputs if o["OutputKey"] == "AuthAtEdgeUserPoolId"
+                o["OutputValue"]
+                for o in outputs
+                if ("OutputKey" in o and "OutputValue" in o)
+                and o["OutputKey"] == "AuthAtEdgeUserPoolId"
             )
 
-        client_id = next(o["OutputValue"] for o in outputs if o["OutputKey"] == "AuthAtEdgeClient")
+        client_id = next(
+            o["OutputValue"]
+            for o in outputs
+            if ("OutputKey" in o and "OutputValue" in o) and o["OutputKey"] == "AuthAtEdgeClient"
+        )
 
         # Poll the user pool client information
         resp = cognito_client.describe_user_pool_client(UserPoolId=user_pool_id, ClientId=client_id)
 
         # Retrieve the callbacks
-        callbacks = resp["UserPoolClient"]["CallbackURLs"]
+        callbacks = resp["UserPoolClient"].get("CallbackURLs")
 
         if callbacks:
             context_dict["callback_urls"] = callbacks
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/client_updater.py b/runway/cfngin/hooks/staticsite/auth_at_edge/client_updater.py
index e4be5b8ad..c4fce55a9 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/client_updater.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/client_updater.py
@@ -8,7 +8,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, List  # noqa: UP035
+from typing import TYPE_CHECKING, Any
 
 from ...base import HookArgsBaseModel
 
@@ -21,7 +21,7 @@
 class HookArgs(HookArgsBaseModel):
     """Hook arguments."""
 
-    alternate_domains: List[str]  # noqa: UP006
+    alternate_domains: list[str]
     """A list of any alternate domains that need to be listed with the primary
     distribution domain.
 
@@ -33,7 +33,7 @@ class HookArgs(HookArgsBaseModel):
     distribution_domain: str
     """Distribution domain."""
 
-    oauth_scopes: List[str]  # noqa: UP006
+    oauth_scopes: list[str]
     """A list of all available validation scopes for oauth."""
 
     redirect_path_sign_in: str
@@ -42,7 +42,7 @@ class HookArgs(HookArgsBaseModel):
     redirect_path_sign_out: str
     """The redirect path after sign out."""
 
-    supported_identity_providers: List[str] = []  # noqa: UP006
+    supported_identity_providers: list[str] = []
     """Supported identity providers."""
 
 
@@ -70,7 +70,7 @@ def update(context: CfnginContext, *_args: Any, **kwargs: Any) -> bool:
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     session = context.get_session()
     cognito_client = session.client("cognito-idp")
 
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/domain_updater.py b/runway/cfngin/hooks/staticsite/auth_at_edge/domain_updater.py
index eb9754989..b82c9926d 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/domain_updater.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/domain_updater.py
@@ -35,7 +35,7 @@ def update(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict[str, Any]
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     session = context.get_session()
     cognito_client = session.client("cognito-idp")
 
@@ -79,7 +79,7 @@ def delete(context: CfnginContext, *_args: Any, **kwargs: Any) -> dict[str, Any]
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     session = context.get_session()
     cognito_client = session.client("cognito-idp")
 
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/lambda_config.py b/runway/cfngin/hooks/staticsite/auth_at_edge/lambda_config.py
index 97e67494a..8831681ac 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/lambda_config.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/lambda_config.py
@@ -9,7 +9,7 @@
 import shutil
 import tempfile
 from tempfile import mkstemp
-from typing import TYPE_CHECKING, Any, Dict, List, Optional  # noqa: UP035
+from typing import TYPE_CHECKING, Any
 
 from ... import aws_lambda
 from ...base import HookArgsBaseModel
@@ -34,16 +34,16 @@ class HookArgs(HookArgsBaseModel):
     client_id: str
     """The ID of the Cognito User Pool Client."""
 
-    cookie_settings: Dict[str, Any]  # noqa: UP006
+    cookie_settings: dict[str, Any]
     """The settings for our customized cookies."""
 
-    http_headers: Dict[str, Any]  # noqa: UP006
+    http_headers: dict[str, Any]
     """The additional headers added to our requests."""
 
     nonce_signing_secret_param_name: str
     """SSM param name to store nonce signing secret."""
 
-    oauth_scopes: List[str]  # noqa: UP006
+    oauth_scopes: list[str]
     """The validation scopes for our OAuth requests."""
 
     redirect_path_refresh: str
@@ -58,7 +58,7 @@ class HookArgs(HookArgsBaseModel):
 
     """
 
-    required_group: Optional[str] = None
+    required_group: str | None = None
     """Optional User Pool group to which access should be restricted."""
 
 
@@ -77,7 +77,7 @@ def write(
 
     """
     cognito_domain = context.hook_data["aae_domain_updater"].get("domain")
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     config = {
         "client_id": args.client_id,
         "cognito_auth_domain": cognito_domain,
@@ -177,7 +177,7 @@ def get_nonce_signing_secret(param_name: str, context: CfnginContext) -> str:
     ssm_client = session.client("ssm")
     try:
         response = ssm_client.get_parameter(Name=param_name, WithDecryption=True)
-        return response["Parameter"]["Value"]
+        return response["Parameter"].get("Value", "")
     except ssm_client.exceptions.ParameterNotFound:
         secret = random_key(16)
         ssm_client.put_parameter(
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/templates/shared_jose.py b/runway/cfngin/hooks/staticsite/auth_at_edge/templates/shared_jose.py
index b6b1c2567..0c0cffb41 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/templates/shared_jose.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/templates/shared_jose.py
@@ -92,7 +92,7 @@ def __init__(self, options=None):
         """Initialize.
 
         Args:
-            options (Optional[dict[str, str]]): Options for the client.
+            options: Options for the client.
 
         """
         self.options = options
diff --git a/runway/cfngin/hooks/staticsite/auth_at_edge/user_pool_id_retriever.py b/runway/cfngin/hooks/staticsite/auth_at_edge/user_pool_id_retriever.py
index 5594861fa..1ed016824 100644
--- a/runway/cfngin/hooks/staticsite/auth_at_edge/user_pool_id_retriever.py
+++ b/runway/cfngin/hooks/staticsite/auth_at_edge/user_pool_id_retriever.py
@@ -1,7 +1,9 @@
 """Retrieve the ID of the Cognito User Pool."""
 
+from __future__ import annotations
+
 import logging
-from typing import Any, Optional
+from typing import Any
 
 from ...base import HookArgsBaseModel
 
@@ -11,10 +13,10 @@
 class HookArgs(HookArgsBaseModel):
     """Hook arguments."""
 
-    created_user_pool_id: Optional[str] = None
+    created_user_pool_id: str | None = None
     """The ID of the created Cognito User Pool."""
 
-    user_pool_arn: Optional[str] = None
+    user_pool_arn: str | None = None
     """The ARN of the supplied User pool."""
 
 
@@ -29,7 +31,7 @@ def get(*__args: Any, **kwargs: Any) -> dict[str, Any]:
     :class:`~runway.cfngin.hooks.staticsite.auth_at_edge.user_pool_id_retriever.HookArgs`.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
 
     # Favor a specific arn over a created one
     if args.user_pool_arn:
diff --git a/runway/cfngin/hooks/staticsite/build_staticsite.py b/runway/cfngin/hooks/staticsite/build_staticsite.py
index 597995266..cf6311517 100644
--- a/runway/cfngin/hooks/staticsite/build_staticsite.py
+++ b/runway/cfngin/hooks/staticsite/build_staticsite.py
@@ -7,13 +7,13 @@
 import tempfile
 import zipfile
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union  # noqa: UP035
+from typing import TYPE_CHECKING, Any
 
 import boto3
-from boto3.s3.transfer import S3Transfer  # type: ignore
+from boto3.s3.transfer import S3Transfer
 from typing_extensions import TypedDict
 
-from ....module.staticsite.options.models import RunwayStaticSiteSourceHashingDataModel
+from ....module.staticsite.options import RunwayStaticSiteSourceHashingDataModel
 from ....s3_utils import does_s3_object_exist, download_and_extract_to_mkdtemp
 from ....utils import change_dir, run_commands
 from ...lookups.handlers.rxref import RxrefLookup
@@ -30,10 +30,10 @@
 class HookArgsOptions(HookArgsBaseModel):
     """Hook arguments ``options`` block."""
 
-    build_output: Optional[str] = None
+    build_output: str | None = None
     """Path were the build static site will be stored locally before upload."""
 
-    build_steps: List[Union[str, List[str], Dict[str, Union[str, List[str]]]]] = []  # noqa: UP006
+    build_steps: list[str | list[str] | dict[str, str | list[str]]] = []
     """Steps to execute to build the static site."""
 
     name: str = "undefined"
@@ -45,9 +45,7 @@ class HookArgsOptions(HookArgsBaseModel):
     path: str
     """Working directory/path to the static site's source code."""
 
-    pre_build_steps: List[  # noqa: UP006
-        Union[str, List[str], Dict[str, Union[str, List[str]]]]  # noqa: UP006
-    ] = []
+    pre_build_steps: list[str | list[str] | dict[str, str | list[str]]] = []
     """Steps to run before building the static site."""
 
     source_hashing: RunwayStaticSiteSourceHashingDataModel = (
@@ -67,11 +65,11 @@ class HookArgs(HookArgsBaseModel):
 
 
 def zip_and_upload(
-    app_dir: str, bucket: str, key: str, session: Optional[boto3.Session] = None
+    app_dir: str, bucket: str, key: str, session: boto3.Session | None = None
 ) -> None:
     """Zip built static site and upload to S3."""
     s3_client = session.client("s3") if session else boto3.client("s3")
-    transfer = S3Transfer(s3_client)  # type: ignore
+    transfer = S3Transfer(s3_client)
 
     filedes, temp_file = tempfile.mkstemp()
     os.close(filedes)
@@ -90,18 +88,18 @@ class OptionsArgTypeDef(TypedDict, total=False):
     """Options argument type definition."""
 
     build_output: str
-    build_steps: list[Union[str, list[str], dict[str, Union[str, list[str]]]]]
+    build_steps: list[str | list[str] | dict[str, str | list[str]]]
     name: str
     namespace: str
     path: str
-    pre_build_steps: list[Union[str, list[str], dict[str, Union[str, list[str]]]]]
+    pre_build_steps: list[str | list[str] | dict[str, str | list[str]]]
 
 
 def build(
     context: CfnginContext,
     provider: Provider,
     *,
-    options: Optional[OptionsArgTypeDef] = None,
+    options: OptionsArgTypeDef | None = None,
     **kwargs: Any,
 ) -> dict[str, Any]:
     """Build static site.
@@ -112,7 +110,7 @@ def build(
     options = options or {}
     options.setdefault("namespace", context.namespace)
     options.setdefault("path", str(context.config_path))
-    args = HookArgs.parse_obj({"options": options, **kwargs})
+    args = HookArgs.model_validate({"options": options, **kwargs})
     session = context.get_session()
 
     context_dict: dict[str, Any] = {
@@ -148,7 +146,7 @@ def build(
         try:
             old_parameter_value = ssm_client.get_parameter(
                 Name=context_dict["hash_tracking_parameter"]
-            )["Parameter"]["Value"]
+            )["Parameter"].get("Value")
         except ssm_client.exceptions.ParameterNotFound:
             old_parameter_value = None
     else:
diff --git a/runway/cfngin/hooks/staticsite/cleanup.py b/runway/cfngin/hooks/staticsite/cleanup.py
index 4692e2513..67d27e200 100644
--- a/runway/cfngin/hooks/staticsite/cleanup.py
+++ b/runway/cfngin/hooks/staticsite/cleanup.py
@@ -64,7 +64,7 @@ def warn(context: CfnginContext, *_args: Any, **kwargs: Any) -> bool:
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     cfn_client = context.get_session().client("cloudformation")
     try:
         describe_response = cfn_client.describe_stacks(
@@ -75,7 +75,7 @@ def warn(context: CfnginContext, *_args: Any, **kwargs: Any) -> bool:
             for x in describe_response.get("Stacks", [])
             if (x.get("StackStatus") and x.get("StackStatus") not in STACK_STATUSES_TO_IGNORE)
         )
-        functions = get_replicated_function_names(stack["Outputs"])
+        functions = get_replicated_function_names(stack.get("Outputs", []))
         if functions:
             cmd = (
                 "aws lambda delete-function --function-name $x "
diff --git a/runway/cfngin/hooks/staticsite/upload_staticsite.py b/runway/cfngin/hooks/staticsite/upload_staticsite.py
index b5183dc53..e51306775 100644
--- a/runway/cfngin/hooks/staticsite/upload_staticsite.py
+++ b/runway/cfngin/hooks/staticsite/upload_staticsite.py
@@ -8,12 +8,12 @@
 import os
 import time
 from operator import itemgetter
-from typing import TYPE_CHECKING, Any, List, Optional, cast  # noqa: UP035
+from typing import TYPE_CHECKING, Any, cast
 
 import yaml
 
 from ....core.providers.aws.s3 import Bucket
-from ....module.staticsite.options.models import RunwayStaticSiteExtraFileDataModel
+from ....module.staticsite.options import RunwayStaticSiteExtraFileDataModel
 from ....utils import JsonEncoder
 from ..base import HookArgsBaseModel
 
@@ -43,10 +43,10 @@ class HookArgs(HookArgsBaseModel):
     distribution_path: str = "/*"
     """Path in the CloudFront distribution to invalidate."""
 
-    extra_files: List[RunwayStaticSiteExtraFileDataModel] = []  # noqa: UP006
+    extra_files: list[RunwayStaticSiteExtraFileDataModel] = []
     """Extra files to sync to the S3 bucket."""
 
-    website_url: Optional[str] = None
+    website_url: str | None = None
     """S3 bucket website URL."""
 
 
@@ -80,7 +80,7 @@ def sync(context: CfnginContext, *__args: Any, **kwargs: Any) -> bool:
         **kwargs: Arbitrary keyword arguments.
 
     """
-    args = HookArgs.parse_obj(kwargs)
+    args = HookArgs.model_validate(kwargs)
     session = context.get_session()
     build_context = context.hook_data["staticsite"]
     invalidate_cache = False
@@ -215,7 +215,7 @@ def prune_archives(context: CfnginContext, session: Session) -> bool:
     return True
 
 
-def auto_detect_content_type(filename: Optional[str]) -> Optional[str]:
+def auto_detect_content_type(filename: str | None) -> str | None:
     """Auto detects the content type based on the filename.
 
     Args:
@@ -239,7 +239,7 @@ def auto_detect_content_type(filename: Optional[str]) -> Optional[str]:
     return None
 
 
-def get_content_type(extra_file: RunwayStaticSiteExtraFileDataModel) -> Optional[str]:
+def get_content_type(extra_file: RunwayStaticSiteExtraFileDataModel) -> str | None:
     """Return the content type of the file.
 
     Args:
@@ -253,7 +253,7 @@ def get_content_type(extra_file: RunwayStaticSiteExtraFileDataModel) -> Optional
     return extra_file.content_type or auto_detect_content_type(extra_file.name)
 
 
-def get_content(extra_file: RunwayStaticSiteExtraFileDataModel) -> Optional[str]:
+def get_content(extra_file: RunwayStaticSiteExtraFileDataModel) -> str | None:
     """Get serialized content based on content_type.
 
     Args:
@@ -276,7 +276,7 @@ def get_content(extra_file: RunwayStaticSiteExtraFileDataModel) -> Optional[str]
         if not isinstance(extra_file.content, str):
             raise TypeError(f"unsupported content: {type(extra_file.content)}")
 
-    return cast(Optional[str], extra_file.content)
+    return cast("str | None", extra_file.content)
 
 
 def calculate_hash_of_extra_files(
@@ -318,7 +318,7 @@ def calculate_hash_of_extra_files(
     return file_hash.hexdigest()
 
 
-def get_ssm_value(session: Session, name: str) -> Optional[str]:
+def get_ssm_value(session: Session, name: str) -> str | None:
     """Get the ssm parameter value.
 
     Args:
@@ -332,7 +332,7 @@ def get_ssm_value(session: Session, name: str) -> Optional[str]:
     ssm_client = session.client("ssm")
 
     try:
-        return ssm_client.get_parameter(Name=name)["Parameter"]["Value"]
+        return ssm_client.get_parameter(Name=name)["Parameter"].get("Value")
     except ssm_client.exceptions.ParameterNotFound:
         return None
 
diff --git a/runway/cfngin/hooks/staticsite/utils.py b/runway/cfngin/hooks/staticsite/utils.py
index 6450fa193..67a50c96c 100644
--- a/runway/cfngin/hooks/staticsite/utils.py
+++ b/runway/cfngin/hooks/staticsite/utils.py
@@ -6,7 +6,7 @@
 import logging
 import os
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union, cast
+from typing import TYPE_CHECKING, cast
 
 import igittigitt
 
@@ -38,7 +38,7 @@ def calculate_hash_of_files(files: Iterable[StrPath], root: Path) -> str:
 
 def get_hash_of_files(
     root_path: Path,
-    directories: Optional[list[dict[str, Union[list[str], str]]]] = None,
+    directories: list[dict[str, list[str] | str | None]] | None = None,
 ) -> str:
     """Generate md5 hash of files.
 
@@ -62,8 +62,8 @@ def get_hash_of_files(
             for root, dirs, files in os.walk(cast(str, i["path"]), topdown=True):
                 sub_root = Path(root).resolve()
                 if root != "./" and gitignore.match(sub_root):
-                    dirs[:] = []  # type: ignore
-                    files[:] = []  # type: ignore
+                    dirs[:] = []
+                    files[:] = []
                 else:
                     for filename in files:
                         filepath = sub_root / filename
diff --git a/runway/cfngin/hooks/utils.py b/runway/cfngin/hooks/utils.py
index 87f653ab7..59edd85a2 100644
--- a/runway/cfngin/hooks/utils.py
+++ b/runway/cfngin/hooks/utils.py
@@ -6,7 +6,7 @@
 import logging
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Annotated, Any
 
 import pydantic
 
@@ -34,18 +34,10 @@ def create_template(self) -> None:
 class TagDataModel(BaseModel):
     """AWS Resource Tag data model."""
 
-    key: str
-    value: str
+    model_config = pydantic.ConfigDict(extra="forbid", populate_by_name=True)
 
-    class Config:
-        """Model configuration."""
-
-        allow_population_by_field_name = True
-        extra = pydantic.Extra.forbid
-        fields = {
-            "key": {"alias": "Key"},
-            "value": {"alias": "Value"},
-        }
+    key: Annotated[str, pydantic.Field(alias="Key")]
+    value: Annotated[str, pydantic.Field(alias="Value")]
 
 
 def full_path(path: str) -> str:
@@ -119,7 +111,7 @@ def handle_hooks(  # noqa: C901, PLR0912, PLR0915
             if isinstance(method, type):
                 result: Any = getattr(method(context=context, provider=provider, **kwargs), stage)()
             else:
-                result = cast(Any, method(context=context, provider=provider, **kwargs))
+                result = method(context=context, provider=provider, **kwargs)
         except Exception:
             LOGGER.exception("hook %s threw an exception", hook.path)
             if hook.required:
diff --git a/runway/cfngin/lookups/handlers/ami.py b/runway/cfngin/lookups/handlers/ami.py
index 121c787cb..f709fe129 100644
--- a/runway/cfngin/lookups/handlers/ami.py
+++ b/runway/cfngin/lookups/handlers/ami.py
@@ -5,16 +5,15 @@
 
 import operator
 import re
-from typing import TYPE_CHECKING, Any, Final, List, Optional, Union  # noqa: UP035
+from typing import TYPE_CHECKING, Any, ClassVar
 
-from pydantic import validator
+from pydantic import field_validator
 
 from ....lookups.handlers.base import LookupHandler
 from ....utils import BaseModel
 from ...utils import read_value_from_path
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ....context import CfnginContext
 
@@ -27,21 +26,22 @@ class ArgsDataModel(BaseModel):
 
     """
 
-    executable_users: Optional[List[str]] = None  # noqa: UP006
+    executable_users: list[str] | None = None
     """List of executable users."""
 
-    owners: List[str]  # noqa: UP006
+    owners: list[str]
     """At least one owner is required.
 
     Should be ``amazon``, ``self``, or an AWS account ID.
 
     """
 
-    region: Optional[str] = None
+    region: str | None = None
     """AWS region."""
 
-    @validator("executable_users", "owners", allow_reuse=True, pre=True)  # type: ignore
-    def _convert_str_to_list(cls, v: Union[list[str], str]) -> list[str]:  # noqa: N805
+    @field_validator("executable_users", "owners", mode="before")
+    @classmethod
+    def _convert_str_to_list(cls, v: list[str] | str) -> list[str]:
         """Convert str to list."""
         if isinstance(v, str):
             return v.split(",")
@@ -62,7 +62,7 @@ def __init__(self, search_string: str) -> None:
 class AmiLookup(LookupHandler):
     """AMI lookup."""
 
-    TYPE_NAME: Final[Literal["ami"]] = "ami"
+    TYPE_NAME: ClassVar[str] = "ami"
     """Name that the Lookup is registered as."""
 
     @classmethod
@@ -114,14 +114,14 @@ def handle(cls, value: str, context: CfnginContext, *__args: Any, **__kwargs: An
 
         """
         query, raw_args = cls.parse(value)
-        args = ArgsDataModel.parse_obj(raw_args)
+        args = ArgsDataModel.model_validate(raw_args)
         ec2 = context.get_session(region=args.region).client("ec2")
 
         describe_args: dict[str, Any] = {
             "Filters": [
                 {"Name": key, "Values": val.split(",") if val else val}
                 for key, val in {
-                    k: v for k, v in raw_args.items() if k not in ArgsDataModel.__fields__
+                    k: v for k, v in raw_args.items() if k not in ArgsDataModel.model_fields
                 }.items()
             ],
             "Owners": args.owners,
diff --git a/runway/cfngin/lookups/handlers/awslambda.py b/runway/cfngin/lookups/handlers/awslambda.py
index 682783422..e34aa7b33 100644
--- a/runway/cfngin/lookups/handlers/awslambda.py
+++ b/runway/cfngin/lookups/handlers/awslambda.py
@@ -13,7 +13,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from pydantic import ValidationError
 from troposphere.awslambda import Code, Content
@@ -23,7 +23,6 @@
 from ...exceptions import CfnginOnlyLookupError
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ....config import CfnginConfig
     from ....config.models.cfngin import CfnginHookDefinitionModel
@@ -37,7 +36,7 @@
 class AwsLambdaLookup(LookupHandler):
     """Lookup for AwsLambdaHook responses."""
 
-    TYPE_NAME: Final[Literal["awslambda"]] = "awslambda"
+    TYPE_NAME: ClassVar[str] = "awslambda"
 
     @classmethod
     def get_deployment_package_data(
@@ -74,7 +73,7 @@ def get_deployment_package_data(
             )
             context.set_hook_data(data_key, hook.plan())
         try:
-            return _AwsLambdaHookDeployResponse.parse_obj(context.hook_data[data_key])
+            return _AwsLambdaHookDeployResponse.model_validate(context.hook_data[data_key])
         except ValidationError:
             raise TypeError(
                 "expected AwsLambdaHookDeployResponseTypedDict, "
@@ -115,7 +114,7 @@ def get_required_hook_definition(
     def handle(
         cls,
         value: str,
-        context: Union[CfnginContext, RunwayContext],
+        context: CfnginContext | RunwayContext,
         *_args: Any,
         **_kwargs: Any,
     ) -> AwsLambdaHookDeployResponse:
@@ -171,13 +170,13 @@ def init_hook_class(
     class Code(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.Code"]] = "awslambda.Code"
+        TYPE_NAME: ClassVar[str] = "awslambda.Code"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> Code:
@@ -195,7 +194,7 @@ def handle(
 
             """
             return Code(
-                **AwsLambdaLookup.handle(value, context, *args, **kwargs).dict(
+                **AwsLambdaLookup.handle(value, context, *args, **kwargs).model_dump(
                     by_alias=True,
                     exclude_none=True,
                     include={"bucket_name", "object_key", "object_version_id"},
@@ -205,13 +204,13 @@ def handle(
     class CodeSha256(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.CodeSha256"]] = "awslambda.CodeSha256"
+        TYPE_NAME: ClassVar[str] = "awslambda.CodeSha256"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> str:
@@ -233,18 +232,16 @@ def handle(
     class CompatibleArchitectures(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.CompatibleArchitectures"]] = (
-            "awslambda.CompatibleArchitectures"
-        )
+        TYPE_NAME: ClassVar[str] = "awslambda.CompatibleArchitectures"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
-        ) -> Optional[list[str]]:
+        ) -> list[str] | None:
             """Retrieve metadata for an AWS Lambda deployment package.
 
             Args:
@@ -267,13 +264,13 @@ def handle(
     class CompatibleRuntimes(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.CompatibleRuntimes"]] = "awslambda.CompatibleRuntimes"
+        TYPE_NAME: ClassVar[str] = "awslambda.CompatibleRuntimes"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> Any:
@@ -299,13 +296,13 @@ def handle(
     class Content(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.Content"]] = "awslambda.Content"
+        TYPE_NAME: ClassVar[str] = "awslambda.Content"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> Content:
@@ -323,7 +320,7 @@ def handle(
 
             """
             return Content(
-                **AwsLambdaLookup.handle(value, context, *args, **kwargs).dict(
+                **AwsLambdaLookup.handle(value, context, *args, **kwargs).model_dump(
                     by_alias=True,
                     exclude_none=True,
                     include={"bucket_name", "object_key", "object_version_id"},
@@ -333,16 +330,16 @@ def handle(
     class LicenseInfo(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.LicenseInfo"]] = "awslambda.LicenseInfo"
+        TYPE_NAME: ClassVar[str] = "awslambda.LicenseInfo"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
-        ) -> Optional[str]:
+        ) -> str | None:
             """Retrieve metadata for an AWS Lambda deployment package.
 
             Args:
@@ -365,13 +362,13 @@ def handle(
     class Runtime(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.Runtime"]] = "awslambda.Runtime"
+        TYPE_NAME: ClassVar[str] = "awslambda.Runtime"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> str:
@@ -393,13 +390,13 @@ def handle(
     class S3Bucket(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.S3Bucket"]] = "awslambda.S3Bucket"
+        TYPE_NAME: ClassVar[str] = "awslambda.S3Bucket"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> str:
@@ -422,13 +419,13 @@ def handle(
     class S3Key(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.S3Key"]] = "awslambda.S3Key"
+        TYPE_NAME: ClassVar[str] = "awslambda.S3Key"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
         ) -> str:
@@ -451,16 +448,16 @@ def handle(
     class S3ObjectVersion(LookupHandler):
         """Lookup for AwsLambdaHook responses."""
 
-        TYPE_NAME: Final[Literal["awslambda.S3ObjectVersion"]] = "awslambda.S3ObjectVersion"
+        TYPE_NAME: ClassVar[str] = "awslambda.S3ObjectVersion"
 
         @classmethod
         def handle(
             cls,
             value: str,
-            context: Union[CfnginContext, RunwayContext],
+            context: CfnginContext | RunwayContext,
             *args: Any,
             **kwargs: Any,
-        ) -> Optional[str]:
+        ) -> str | None:
             """Retrieve metadata for an AWS Lambda deployment package.
 
             Args:
diff --git a/runway/cfngin/lookups/handlers/default.py b/runway/cfngin/lookups/handlers/default.py
index 0d55df1d5..413dc38ae 100644
--- a/runway/cfngin/lookups/handlers/default.py
+++ b/runway/cfngin/lookups/handlers/default.py
@@ -3,12 +3,11 @@
 # pyright: reportIncompatibleMethodOverride=none
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Final, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ....lookups.handlers.base import LookupHandler
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ....context import CfnginContext
 
@@ -16,11 +15,11 @@
 class DefaultLookup(LookupHandler):
     """Lookup to provide a default value."""
 
-    TYPE_NAME: Final[Literal["default"]] = "default"
+    TYPE_NAME: ClassVar[str] = "default"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def handle(cls, value: str, context: Optional[CfnginContext] = None, **_: Any) -> Any:
+    def handle(cls, value: str, context: CfnginContext | None = None, **_: Any) -> Any:
         """Use a value from the environment or fall back to a default value.
 
         Allows defaults to be set at the config file level.
diff --git a/runway/cfngin/lookups/handlers/dynamodb.py b/runway/cfngin/lookups/handlers/dynamodb.py
index 3701b822b..22c7ce34c 100644
--- a/runway/cfngin/lookups/handlers/dynamodb.py
+++ b/runway/cfngin/lookups/handlers/dynamodb.py
@@ -4,7 +4,7 @@
 from __future__ import annotations
 
 import re
-from typing import TYPE_CHECKING, Any, Final, Optional, cast
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from botocore.exceptions import ClientError
 from typing_extensions import Literal, TypedDict
@@ -14,7 +14,10 @@
 from ...utils import read_value_from_path
 
 if TYPE_CHECKING:
+    from mypy_boto3_dynamodb.type_defs import AttributeValueTypeDef
+
     from ....context import CfnginContext
+    from ....lookups.handlers.base import ParsedArgsTypeDef
 
 
 _QUERY_PATTERN = r"""(?x)  # <table_name>@<partition_key>:<partition_key_value>.<attribute>
@@ -38,7 +41,7 @@
 class ArgsDataModel(BaseModel):
     """Arguments data model."""
 
-    region: Optional[str] = None
+    region: str | None = None
     """AWS region."""
 
 
@@ -61,7 +64,7 @@ class QueryDataModel(BaseModel):
     """Name of the DynamoDB Table to query."""
 
     @property
-    def item_key(self) -> dict[str, dict[Literal["B", "N", "S"], Any]]:
+    def item_key(self) -> dict[str, AttributeValueTypeDef]:
         """Value to pass to boto3 ``.get_item()`` call as the ``Key`` argument.
 
         Raises:
@@ -77,22 +80,21 @@ def item_key(self) -> dict[str, dict[Literal["B", "N", "S"], Any]]:
                 f"doesn't match regex: {pattern.pattern}"
             )
         return {
-            self.partition_key: {
-                cast(Literal["B", "N", "S"], match.groupdict("S")["data_type"]): match.group(
-                    "value"
-                )
-            }
+            self.partition_key: cast(
+                "AttributeValueTypeDef",
+                {match.groupdict("S")["data_type"]: match.group("value")},
+            )
         }
 
 
 class DynamodbLookup(LookupHandler):
     """DynamoDB lookup."""
 
-    TYPE_NAME: Final[Literal["dynamodb"]] = "dynamodb"
+    TYPE_NAME: ClassVar[str] = "dynamodb"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def parse(cls, value: str) -> tuple[str, dict[str, str]]:
+    def parse(cls, value: str) -> tuple[str, ParsedArgsTypeDef]:
         """Parse the value passed to the lookup.
 
         This overrides the default parsing to account for special requirements.
@@ -109,7 +111,7 @@ def parse(cls, value: str) -> tuple[str, dict[str, str]]:
 
         """
         raw_value = read_value_from_path(value)
-        args: dict[str, str] = {}
+        args: ParsedArgsTypeDef = {}
 
         if "@" not in raw_value:
             raise ValueError(
@@ -136,7 +138,7 @@ def parse_query(cls, value: str) -> QueryDataModel:
         match = pattern.search(value)
         if not match:
             raise ValueError(f"Query '{value}' doesn't match regex:\n{pattern.pattern}")
-        return QueryDataModel.parse_obj(match.groupdict())
+        return QueryDataModel.model_validate(match.groupdict())
 
     @classmethod
     def handle(cls, value: str, context: CfnginContext, *__args: Any, **__kwargs: Any) -> Any:
@@ -156,7 +158,7 @@ def handle(cls, value: str, context: CfnginContext, *__args: Any, **__kwargs: An
         """
         raw_query, raw_args = cls.parse(value)
         query = cls.parse_query(raw_query)
-        args = ArgsDataModel.parse_obj(raw_args)
+        args = ArgsDataModel.model_validate(raw_args)
 
         table_keys = query.attribute.split(".")
 
@@ -229,7 +231,9 @@ def _lookup_key_parse(table_keys: list[str]) -> ParsedLookupKey:
     return {"new_keys": new_keys, "clean_table_keys": clean_table_keys}
 
 
-def _get_val_from_ddb_data(data: dict[str, Any], keylist: list[dict[str, str]]) -> Any:
+def _get_val_from_ddb_data(
+    data: dict[str, Any], keylist: list[dict[Literal["L", "M", "N", "S"], str]]
+) -> Any:
     """Return the value of the lookup.
 
     Args:
@@ -241,7 +245,7 @@ def _get_val_from_ddb_data(data: dict[str, Any], keylist: list[dict[str, str]])
         datatype.
 
     """
-    next_type: Optional[str] = None
+    next_type: str | None = None
     # iterate through the keylist to find the matching key/datatype
     for key in keylist:
         for k in key:
diff --git a/runway/cfngin/lookups/handlers/envvar.py b/runway/cfngin/lookups/handlers/envvar.py
index 3c93ef447..a22ec5f81 100644
--- a/runway/cfngin/lookups/handlers/envvar.py
+++ b/runway/cfngin/lookups/handlers/envvar.py
@@ -3,9 +3,7 @@
 # pyright: reportIncompatibleMethodOverride=none
 import logging
 import os
-from typing import Any, Final
-
-from typing_extensions import Literal
+from typing import Any, ClassVar
 
 from ....lookups.handlers.base import LookupHandler
 from ...utils import read_value_from_path
@@ -17,7 +15,7 @@ class EnvvarLookup(LookupHandler):
     """Environment variable lookup."""
 
     DEPRECATION_MSG = "envvar Lookup has been deprecated; use the env lookup instead"
-    TYPE_NAME: Final[Literal["envvar"]] = "envvar"
+    TYPE_NAME: ClassVar[str] = "envvar"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/cfngin/lookups/handlers/file.py b/runway/cfngin/lookups/handlers/file.py
index aa7223290..56df09811 100644
--- a/runway/cfngin/lookups/handlers/file.py
+++ b/runway/cfngin/lookups/handlers/file.py
@@ -7,11 +7,10 @@
 import collections.abc
 import json
 import re
-from collections.abc import Mapping, Sequence
-from typing import TYPE_CHECKING, Any, Callable, Final, Union, overload
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, overload
 
 import yaml
-from pydantic import validator
+from pydantic import field_validator
 from troposphere import Base64, GenericHelperFn
 
 from ....lookups.handlers.base import LookupHandler
@@ -19,16 +18,18 @@
 from ...utils import read_value_from_path
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
+    from collections.abc import Mapping, Sequence
+
+    from typing_extensions import Literal, TypeAlias
+
+    from ....lookups.handlers.base import ParsedArgsTypeDef
 
 _PARAMETER_PATTERN = re.compile(r"{{([::|\w]+)}}")
 
-ParameterizedObjectTypeDef = Union[str, Mapping[str, Any], Sequence[Any], Any]
-ParameterizedObjectReturnTypeDef = Union[
-    dict[str, "ParameterizedObjectReturnTypeDef"],
-    GenericHelperFn,
-    list["ParameterizedObjectReturnTypeDef"],
-]
+ParameterizedObjectTypeDef: TypeAlias = "str | Mapping[str, Any] | Sequence[Any] | Any"
+ParameterizedObjectReturnTypeDef: TypeAlias = (
+    "dict[str, ParameterizedObjectReturnTypeDef] | GenericHelperFn | list[ParameterizedObjectReturnTypeDef]"
+)
 
 
 class ArgsDataModel(BaseModel):
@@ -37,8 +38,9 @@ class ArgsDataModel(BaseModel):
     codec: str
     """Codec that will be used to parse and/or manipulate the data."""
 
-    @validator("codec", allow_reuse=True)  # type: ignore
-    def _validate_supported_codec(cls, v: str) -> str:  # noqa: N805
+    @field_validator("codec")
+    @classmethod
+    def _validate_supported_codec(cls, v: str) -> str:
         """Validate that the selected codec is supported."""
         if v in CODECS:
             return v
@@ -48,11 +50,11 @@ def _validate_supported_codec(cls, v: str) -> str:  # noqa: N805
 class FileLookup(LookupHandler):
     """File lookup."""
 
-    TYPE_NAME: Final[Literal["file"]] = "file"
+    TYPE_NAME: ClassVar[str] = "file"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def parse(cls, value: str) -> tuple[str, dict[str, str]]:
+    def parse(cls, value: str) -> tuple[str, ParsedArgsTypeDef]:
         """Parse the value passed to the lookup.
 
         This overrides the default parsing to account for special requirements.
@@ -67,9 +69,11 @@ def parse(cls, value: str) -> tuple[str, dict[str, str]]:
             ValueError: The value provided does not match the expected regex.
 
         """
-        args: dict[str, str] = {}
+        args: ParsedArgsTypeDef = {}
         try:
-            args["codec"], data_or_path = value.split(":", 1)
+            args["codec"], data_or_path = value.split(  # pyright: ignore[reportGeneralTypeIssues]
+                ":", 1
+            )
         except ValueError:
             raise ValueError(
                 rf"Query '{value}' doesn't match regex: ^(?P<codec>[{'|'.join(CODECS)}]:.+$)"
@@ -80,7 +84,7 @@ def parse(cls, value: str) -> tuple[str, dict[str, str]]:
     def handle(cls, value: str, **_: Any) -> Any:
         """Translate a filename into the file contents."""
         data, raw_args = cls.parse(value)
-        args = ArgsDataModel.parse_obj(raw_args)
+        args = ArgsDataModel.model_validate(raw_args)
         return CODECS[args.codec](data)
 
 
@@ -141,7 +145,7 @@ def parameterized_codec(raw: str, b64: bool = False) -> Any:
 
 
 @overload
-def _parameterize_obj(obj: Union[bytes, str]) -> GenericHelperFn: ...
+def _parameterize_obj(obj: bytes | str) -> GenericHelperFn: ...
 
 
 @overload
@@ -180,13 +184,13 @@ def _parameterize_obj(
 
 def yaml_codec(raw: str, parameterized: bool = False) -> Any:
     """YAML codec."""
-    data = yaml.load(raw, Loader=yaml.SafeLoader)
+    data: Mapping[str, Any] = yaml.load(raw, Loader=yaml.SafeLoader)
     return _parameterize_obj(data) if parameterized else data
 
 
 def json_codec(raw: str, parameterized: bool = False) -> Any:
     """JSON codec."""
-    data = json.loads(raw)
+    data: Mapping[str, Any] = json.loads(raw)
     return _parameterize_obj(data) if parameterized else data
 
 
diff --git a/runway/cfngin/lookups/handlers/hook_data.py b/runway/cfngin/lookups/handlers/hook_data.py
index 1736cc677..c86ac4d6b 100644
--- a/runway/cfngin/lookups/handlers/hook_data.py
+++ b/runway/cfngin/lookups/handlers/hook_data.py
@@ -4,7 +4,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from troposphere import BaseAWSObject
 
@@ -12,7 +12,6 @@
 from ....utils import MutableMap
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ....context import CfnginContext
 
@@ -22,7 +21,7 @@
 class HookDataLookup(LookupHandler):
     """Hook data lookup."""
 
-    TYPE_NAME: Final[Literal["hook_data"]] = "hook_data"
+    TYPE_NAME: ClassVar[str] = "hook_data"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/cfngin/lookups/handlers/kms.py b/runway/cfngin/lookups/handlers/kms.py
index c9aef5e1c..b71ecc137 100644
--- a/runway/cfngin/lookups/handlers/kms.py
+++ b/runway/cfngin/lookups/handlers/kms.py
@@ -5,16 +5,15 @@
 
 import codecs
 import logging
-from typing import TYPE_CHECKING, Any, BinaryIO, Final, Union, cast
+from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, cast
 
 from ....lookups.handlers.base import LookupHandler
 from ....utils import DOC_SITE
 from ...utils import read_value_from_path
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
-
     from ....context import CfnginContext
+    from ....lookups.handlers.base import ParsedArgsTypeDef
 
 LOGGER = logging.getLogger(__name__)
 
@@ -27,11 +26,11 @@ class KmsLookup(LookupHandler):
         "to learn how to use the new lookup query syntax visit "
         f"{DOC_SITE}/page/cfngin/lookups/kms.html"
     )
-    TYPE_NAME: Final[Literal["kms"]] = "kms"
+    TYPE_NAME: ClassVar[str] = "kms"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def legacy_parse(cls, value: str) -> tuple[str, dict[str, str]]:
+    def legacy_parse(cls, value: str) -> tuple[str, ParsedArgsTypeDef]:
         """Retain support for legacy lookup syntax.
 
         Format of value::
@@ -60,7 +59,7 @@ def handle(cls, value: str, context: CfnginContext, **_: Any) -> str:
         kms = context.get_session(region=args.get("region")).client("kms")
 
         decrypted = cast(
-            Union[BinaryIO, bytes],
+            "BinaryIO | bytes",
             kms.decrypt(CiphertextBlob=codecs.decode(query.encode(), "base64")).get(
                 "Plaintext", b""
             ),
diff --git a/runway/cfngin/lookups/handlers/output.py b/runway/cfngin/lookups/handlers/output.py
index 655b054bd..c0a2fe9db 100644
--- a/runway/cfngin/lookups/handlers/output.py
+++ b/runway/cfngin/lookups/handlers/output.py
@@ -5,7 +5,7 @@
 
 import logging
 import re
-from typing import TYPE_CHECKING, Any, Final, NamedTuple
+from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
 
 from ....exceptions import OutputDoesNotExist
 from ....lookups.handlers.base import LookupHandler
@@ -13,9 +13,9 @@
 from ...exceptions import StackDoesNotExist
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ....context import CfnginContext
+    from ....lookups.handlers.base import ParsedArgsTypeDef
     from ....variables import VariableValue
 
 LOGGER = logging.getLogger(__name__)
@@ -36,11 +36,11 @@ class OutputLookup(LookupHandler):
         "to learn how to use the new lookup query syntax visit "
         f"{DOC_SITE}/page/cfngin/lookups/output.html"
     )
-    TYPE_NAME: Final[Literal["output"]] = "output"
+    TYPE_NAME: ClassVar[str] = "output"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def legacy_parse(cls, value: str) -> tuple[OutputQuery, dict[str, str]]:
+    def legacy_parse(cls, value: str) -> tuple[OutputQuery, ParsedArgsTypeDef]:
         """Retain support for legacy lookup syntax.
 
         Format of value:
diff --git a/runway/cfngin/lookups/handlers/rxref.py b/runway/cfngin/lookups/handlers/rxref.py
index 4eab30edd..8d96ff7e9 100644
--- a/runway/cfngin/lookups/handlers/rxref.py
+++ b/runway/cfngin/lookups/handlers/rxref.py
@@ -4,7 +4,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ....lookups.handlers.base import LookupHandler
 from ....lookups.handlers.cfn import CfnLookup
@@ -12,9 +12,8 @@
 from .output import OutputQuery, deconstruct
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
-
     from ....context import CfnginContext
+    from ....lookups.handlers.base import ParsedArgsTypeDef
     from ...providers.aws.default import Provider
 
 LOGGER = logging.getLogger(__name__)
@@ -28,11 +27,11 @@ class RxrefLookup(LookupHandler):
         "to learn how to use the new lookup query syntax visit "
         f"{DOC_SITE}/page/cfngin/lookups/rxref.html"
     )
-    TYPE_NAME: Final[Literal["rxref"]] = "rxref"
+    TYPE_NAME: ClassVar[str] = "rxref"
     """Name that the Lookup is registered as."""
 
     @classmethod
-    def legacy_parse(cls, value: str) -> tuple[OutputQuery, dict[str, str]]:
+    def legacy_parse(cls, value: str) -> tuple[OutputQuery, ParsedArgsTypeDef]:
         """Retain support for legacy lookup syntax.
 
         Format of value:
diff --git a/runway/cfngin/lookups/handlers/split.py b/runway/cfngin/lookups/handlers/split.py
index 0fd0b9d1a..74f53f405 100644
--- a/runway/cfngin/lookups/handlers/split.py
+++ b/runway/cfngin/lookups/handlers/split.py
@@ -1,9 +1,7 @@
 """Split lookup."""
 
 # pyright: reportIncompatibleMethodOverride=none
-from typing import Any, Final
-
-from typing_extensions import Literal
+from typing import Any, ClassVar
 
 from ....lookups.handlers.base import LookupHandler
 
@@ -11,7 +9,7 @@
 class SplitLookup(LookupHandler):
     """Split lookup."""
 
-    TYPE_NAME: Final[Literal["split"]] = "split"
+    TYPE_NAME: ClassVar[str] = "split"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/cfngin/lookups/handlers/xref.py b/runway/cfngin/lookups/handlers/xref.py
index a057e68de..2143e1a06 100644
--- a/runway/cfngin/lookups/handlers/xref.py
+++ b/runway/cfngin/lookups/handlers/xref.py
@@ -4,13 +4,12 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ....lookups.handlers.base import LookupHandler
 from .output import deconstruct
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ...providers.aws.default import Provider
 
@@ -23,7 +22,7 @@ class XrefLookup(LookupHandler):
     """Xref lookup."""
 
     DEPRECATION_MSG = "xref Lookup has been deprecated; use the cfn lookup instead"
-    TYPE_NAME: Final[Literal["xref"]] = "xref"
+    TYPE_NAME: ClassVar[str] = "xref"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/cfngin/plan.py b/runway/cfngin/plan.py
index af503563e..c32d8aa87 100644
--- a/runway/cfngin/plan.py
+++ b/runway/cfngin/plan.py
@@ -8,29 +8,14 @@
 import time
 import uuid
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    NoReturn,
-    TypeVar,
-    overload,
-)
+from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypeVar, overload
 
 from .._logging import LogLevels, PrefixAdaptor
 from ..utils import merge_dicts
 from .dag import DAG, DAGValidationError, walk
 from .exceptions import CancelExecution, GraphError, PersistentGraphLocked, PlanFailed
 from .stack import Stack
-from .status import (
-    COMPLETE,
-    FAILED,
-    PENDING,
-    SKIPPED,
-    SUBMITTED,
-    FailedStatus,
-    SkippedStatus,
-)
+from .status import COMPLETE, FAILED, PENDING, SKIPPED, SUBMITTED, FailedStatus, SkippedStatus
 from .ui import ui
 from .utils import stack_template_key_name
 
@@ -275,7 +260,9 @@ def from_stack_name(
         """
         from runway.config.models.cfngin import CfnginStackDefinitionModel
 
-        stack_def = CfnginStackDefinitionModel.construct(name=stack_name, requires=requires or [])
+        stack_def = CfnginStackDefinitionModel.model_construct(
+            name=stack_name, requires=requires or []
+        )
         stack = Stack(stack_def, context)
         return cls(stack, fn=fn, watch_func=watch_func)
 
diff --git a/runway/cfngin/providers/aws/default.py b/runway/cfngin/providers/aws/default.py
index 763f71cf4..9c36a05d0 100644
--- a/runway/cfngin/providers/aws/default.py
+++ b/runway/cfngin/providers/aws/default.py
@@ -14,7 +14,6 @@
     TYPE_CHECKING,
     Any,
     Callable,
-    Union,
     cast,
 )
 from urllib.parse import urlparse, urlunparse
@@ -750,7 +749,7 @@ def get_delete_failed_status_reason(self, stack_name: str) -> str | None:
             Reason for the Stack's DELETE_FAILED status if one can be found.
 
         """
-        event: Union[dict[str, str], StackEventTypeDef] = (
+        event: dict[str, str] | StackEventTypeDef = (
             self.get_event_by_resource_status(stack_name, "DELETE_FAILED", chronological=True) or {}
         )
         return event.get("ResourceStatusReason")
@@ -800,9 +799,11 @@ def get_events(
         if chronological:
             return cast(
                 Iterable["StackEventTypeDef"],
-                reversed(cast("list[StackEventTypeDef]", functools.reduce(operator.iadd, event_list, []))),  # type: ignore
+                reversed(
+                    cast("list[StackEventTypeDef]", functools.reduce(operator.iadd, event_list, []))
+                ),
             )
-        return cast(Iterable["StackEventTypeDef"], functools.reduce(operator.iadd, event_list, []))  # type: ignore
+        return cast(Iterable["StackEventTypeDef"], functools.reduce(operator.iadd, event_list, []))
 
     def get_rollback_status_reason(self, stack_name: str) -> str | None:
         """Process events and returns latest roll back reason.
@@ -814,7 +815,7 @@ def get_rollback_status_reason(self, stack_name: str) -> str | None:
             Reason for the Stack's rollback status if one can be found.
 
         """
-        event: Union[dict[str, str], StackEventTypeDef] = (
+        event: dict[str, str] | StackEventTypeDef = (
             self.get_event_by_resource_status(
                 stack_name, "UPDATE_ROLLBACK_IN_PROGRESS", chronological=False
             )
@@ -1283,7 +1284,7 @@ def noninteractive_destroy_stack(self, fqn: str, **_kwargs: Any) -> None:
         if self.service_role:
             args["RoleARN"] = self.service_role
 
-        self.cloudformation.delete_stack(**args)
+        self.cloudformation.delete_stack(**args)  # pyright: ignore[reportArgumentType]
 
     def noninteractive_changeset_update(
         self,
@@ -1414,7 +1415,7 @@ def get_output_dict(stack: StackTypeDef) -> dict[str, str]:
         """Get stack outputs dict."""
         return get_output_dict(stack)
 
-    def get_stack_info(self, stack: StackTypeDef) -> tuple[str, dict[str, Union[list[str], str]]]:
+    def get_stack_info(self, stack: StackTypeDef) -> tuple[str, dict[str, list[str] | str]]:
         """Get the template and parameters of the stack currently in AWS."""
         stack_name = stack.get("StackId", "None")
 
@@ -1428,7 +1429,7 @@ def get_stack_info(self, stack: StackTypeDef) -> tuple[str, dict[str, Union[list
         parameters = self.params_as_dict(stack.get("Parameters", []))
 
         # handle yaml templates
-        if isinstance(template, str):  # type: ignore
+        if isinstance(template, str):
             template = parse_cloudformation_template(template)
 
         return json.dumps(template, cls=JsonEncoder), parameters
@@ -1463,7 +1464,7 @@ def get_stack_changes(  # noqa: C901, PLR0912
             old_template: dict[str, Any] = parse_cloudformation_template(old_template_raw)
             change_type = "UPDATE"
         except exceptions.StackDoesNotExist:
-            old_params: dict[str, Union[list[str], str]] = {}
+            old_params: dict[str, list[str] | str] = {}
             old_template = {}
             change_type = "CREATE"
 
@@ -1579,7 +1580,7 @@ def get_stack_changes(  # noqa: C901, PLR0912
     @staticmethod
     def params_as_dict(
         parameters_list: list[ParameterTypeDef],
-    ) -> dict[str, Union[list[str], str]]:
+    ) -> dict[str, list[str] | str]:
         """Parameters as dict."""
         return {
             param["ParameterKey"]: param["ParameterValue"]  # type: ignore
diff --git a/runway/cfngin/session_cache.py b/runway/cfngin/session_cache.py
index cba1b09d2..8484c73e5 100644
--- a/runway/cfngin/session_cache.py
+++ b/runway/cfngin/session_cache.py
@@ -1,7 +1,8 @@
 """CFNgin session caching."""
 
+from __future__ import annotations
+
 import logging
-from typing import Optional
 
 import boto3
 
@@ -15,11 +16,11 @@
 
 
 def get_session(
-    region: Optional[str] = None,
-    profile: Optional[str] = None,
-    access_key: Optional[str] = None,
-    secret_key: Optional[str] = None,
-    session_token: Optional[str] = None,
+    region: str | None = None,
+    profile: str | None = None,
+    access_key: str | None = None,
+    secret_key: str | None = None,
+    session_token: str | None = None,
 ) -> boto3.Session:
     """Create a thread-safe boto3 session.
 
@@ -51,7 +52,7 @@ def get_session(
         aws_access_key_id=access_key,
         aws_secret_access_key=secret_key,
         aws_session_token=session_token,
-        botocore_session=Session(),  # type: ignore
+        botocore_session=Session(),
         region_name=region,
         profile_name=profile,
     )
diff --git a/runway/cfngin/status.py b/runway/cfngin/status.py
index c7f00dc9b..1c4d6572d 100644
--- a/runway/cfngin/status.py
+++ b/runway/cfngin/status.py
@@ -1,7 +1,9 @@
 """CFNgin statuses."""
 
+from __future__ import annotations
+
 import operator
-from typing import Any, Callable, Optional
+from typing import Any, Callable
 
 
 class Status:
@@ -16,9 +18,9 @@ class Status:
 
     code: int
     name: str
-    reason: Optional[str]
+    reason: str | None
 
-    def __init__(self, name: str, code: int, reason: Optional[str] = None) -> None:
+    def __init__(self, name: str, code: int, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -74,7 +76,7 @@ def __ge__(self, other: Any) -> bool:
 class CompleteStatus(Status):
     """Status name of 'complete' with code of '2'."""
 
-    def __init__(self, reason: Optional[str] = None) -> None:
+    def __init__(self, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -87,7 +89,7 @@ def __init__(self, reason: Optional[str] = None) -> None:
 class FailedStatus(Status):
     """Status name of 'failed' with code of '4'."""
 
-    def __init__(self, reason: Optional[str] = None) -> None:
+    def __init__(self, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -100,7 +102,7 @@ def __init__(self, reason: Optional[str] = None) -> None:
 class PendingStatus(Status):
     """Status name of 'pending' with code of '0'."""
 
-    def __init__(self, reason: Optional[str] = None) -> None:
+    def __init__(self, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -113,7 +115,7 @@ def __init__(self, reason: Optional[str] = None) -> None:
 class SkippedStatus(Status):
     """Status name of 'skipped' with code of '3'."""
 
-    def __init__(self, reason: Optional[str] = None) -> None:
+    def __init__(self, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -126,7 +128,7 @@ def __init__(self, reason: Optional[str] = None) -> None:
 class SubmittedStatus(Status):
     """Status name of 'submitted' with code of '1'."""
 
-    def __init__(self, reason: Optional[str] = None) -> None:
+    def __init__(self, reason: str | None = None) -> None:
         """Instantiate class.
 
         Args:
diff --git a/runway/cfngin/utils.py b/runway/cfngin/utils.py
index 4fa7fd846..c04b5b02d 100644
--- a/runway/cfngin/utils.py
+++ b/runway/cfngin/utils.py
@@ -16,14 +16,7 @@
 import zipfile
 from collections import OrderedDict
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    ClassVar,
-    Optional,
-    Union,
-    cast,
-)
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 import botocore.client
 import botocore.exceptions
@@ -38,7 +31,7 @@
     from collections.abc import Iterator
 
     from mypy_boto3_route53.client import Route53Client
-    from mypy_boto3_route53.type_defs import ResourceRecordSetTypeDef
+    from mypy_boto3_route53.type_defs import ResourceRecordSetExtraOutputTypeDef
     from mypy_boto3_s3.client import S3Client
 
     from ..config.models.cfngin import (
@@ -84,7 +77,7 @@ def parse_zone_id(full_zone_id: str) -> str:
     return full_zone_id.split("/")[2]
 
 
-def get_hosted_zone_by_name(client: Route53Client, zone_name: str) -> Optional[str]:
+def get_hosted_zone_by_name(client: Route53Client, zone_name: str) -> str | None:
     """Get the zone id of an existing zone by name.
 
     Args:
@@ -154,7 +147,7 @@ def __str__(self) -> str:
 class SOARecord:
     """Represents an SOA record."""
 
-    def __init__(self, record: ResourceRecordSetTypeDef) -> None:
+    def __init__(self, record: ResourceRecordSetExtraOutputTypeDef) -> None:
         """Instantiate class."""
         self.name = record["Name"]
         self.text = SOARecordText(record.get("ResourceRecords", [{"Value": ""}])[0]["Value"])
@@ -386,7 +379,7 @@ def get_client_region(client: Any) -> str:
         AWS region string.
 
     """
-    return client._client_config.region_name  # type: ignore  # noqa: SLF001
+    return client._client_config.region_name  # noqa: SLF001
 
 
 def get_s3_endpoint(client: Any) -> str:
@@ -399,10 +392,10 @@ def get_s3_endpoint(client: Any) -> str:
         The AWS endpoint for the client.
 
     """
-    return client._endpoint.host  # type: ignore  # noqa: SLF001
+    return client._endpoint.host  # noqa: SLF001
 
 
-def s3_bucket_location_constraint(region: Optional[str]) -> Optional[str]:
+def s3_bucket_location_constraint(region: str | None) -> str | None:
     """Return the appropriate LocationConstraint info for a new S3 bucket.
 
     When creating a bucket in a region OTHER than us-east-1, you need to
@@ -424,7 +417,7 @@ def s3_bucket_location_constraint(region: Optional[str]) -> Optional[str]:
 def ensure_s3_bucket(
     s3_client: S3Client,
     bucket_name: str,
-    bucket_region: Optional[str] = None,
+    bucket_region: str | None = None,
     *,
     create: bool = True,
     persist_graph: bool = False,
@@ -506,8 +499,8 @@ def is_within_directory(directory: Path | str, target: str) -> bool:
     Determines if the provided path is within a specific directory or its subdirectories.
 
     Args:
-        directory (Union[Path, str]): Path of the directory we're checking.
-        target (str): Path of the file we're checking for containment.
+        directory: Path of the directory we're checking.
+        target: Path of the file we're checking for containment.
 
     Returns:
         bool: True if the target is in the directory or subdirectories, False otherwise.
@@ -553,7 +546,7 @@ class Extractor:
 
     extension: ClassVar[str] = ""
 
-    def __init__(self, archive: Optional[Path] = None) -> None:
+    def __init__(self, archive: Path | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -705,7 +698,7 @@ def fetch_s3_package(self, config: S3CfnginPackageSourceDefinitionModel) -> None
                     session.client("s3")
                     .head_object(Bucket=config.bucket, Key=config.key, **extra_s3_args)[
                         "LastModified"
-                    ]  # type: ignore
+                    ]
                     .astimezone(dateutil.tz.tzutc())  # type: ignore
                 )
             except botocore.exceptions.ClientError as client_error:
@@ -809,13 +802,13 @@ def fetch_git_package(self, config: GitCfnginPackageSourceDefinitionModel) -> No
 
     def update_paths_and_config(
         self,
-        config: Union[
-            GitCfnginPackageSourceDefinitionModel,
-            LocalCfnginPackageSourceDefinitionModel,
-            S3CfnginPackageSourceDefinitionModel,
-        ],
+        config: (
+            GitCfnginPackageSourceDefinitionModel
+            | LocalCfnginPackageSourceDefinitionModel
+            | S3CfnginPackageSourceDefinitionModel
+        ),
         pkg_dir_name: str,
-        pkg_cache_dir: Optional[Path] = None,
+        pkg_cache_dir: Path | None = None,
     ) -> None:
         """Handle remote source defined sys.paths & configs.
 
diff --git a/runway/config/__init__.py b/runway/config/__init__.py
index 61e508fdb..a9fe14c64 100644
--- a/runway/config/__init__.py
+++ b/runway/config/__init__.py
@@ -7,7 +7,7 @@
 import sys
 from pathlib import Path
 from string import Template
-from typing import TYPE_CHECKING, Any, Union, cast
+from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
 
 import yaml
 
@@ -31,21 +31,22 @@
 
 if TYPE_CHECKING:
     from collections.abc import Mapping, MutableMapping
-    from collections.abc import Set as AbstractSet
 
     from packaging.specifiers import SpecifierSet
     from pydantic import BaseModel
 
 LOGGER = logging.getLogger(__name__)
 
+_ModelTypeVar = TypeVar("_ModelTypeVar", bound="BaseModel")
 
-class BaseConfig:
+
+class BaseConfig(Generic[_ModelTypeVar]):
     """Base class for configurations."""
 
     file_path: Path
-    _data: BaseModel
+    _data: _ModelTypeVar
 
-    def __init__(self, data: BaseModel, *, path: Path | None = None) -> None:
+    def __init__(self, data: _ModelTypeVar, *, path: Path | None = None) -> None:
         """Instantiate class.
 
         Args:
@@ -53,18 +54,18 @@ def __init__(self, data: BaseModel, *, path: Path | None = None) -> None:
             path: Path to the config file.
 
         """
-        self._data = data.copy()
+        self._data = data.model_copy()
         self.file_path = path.resolve() if path else Path.cwd()
 
     def dump(
         self,
         *,
         by_alias: bool = False,
-        exclude: Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any]] | None = None,
+        exclude: set[int | str] | Mapping[int | str, Any] | None = None,
         exclude_defaults: bool = False,
         exclude_none: bool = False,
         exclude_unset: bool = True,
-        include: Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any]] | None = None,
+        include: set[int | str] | Mapping[int | str, Any] | None = None,
     ) -> str:
         """Dump model to a YAML string.
 
@@ -84,7 +85,7 @@ def dump(
 
         """
         return yaml.dump(
-            self._data.dict(
+            self._data.model_dump(
                 by_alias=by_alias,
                 exclude=exclude,  # type: ignore
                 exclude_defaults=exclude_defaults,
@@ -106,7 +107,7 @@ def find_config_file(cls, path: Path) -> Path | None:
         raise NotImplementedError  # cov: ignore
 
 
-class CfnginConfig(BaseConfig):
+class CfnginConfig(BaseConfig[CfnginConfigDefinitionModel]):
     """Python representation of a CFNgin config file.
 
     This is used internally by CFNgin to parse and validate a YAML formatted
@@ -194,8 +195,6 @@ class CfnginConfig(BaseConfig):
     template_indent: int
     """Spaces to use per-indent level when outputting a template to json."""
 
-    _data: CfnginConfigDefinitionModel
-
     def __init__(
         self,
         data: CfnginConfigDefinitionModel,
@@ -342,7 +341,7 @@ def parse_obj(
             work_dir: Working directory.
 
         """
-        return cls(CfnginConfigDefinitionModel.parse_obj(obj), path=path, work_dir=work_dir)
+        return cls(CfnginConfigDefinitionModel.model_validate(obj), path=path, work_dir=work_dir)
 
     @classmethod
     def parse_raw(
@@ -393,7 +392,7 @@ def process_package_sources(
         """
         config: dict[str, Any] = yaml.safe_load(raw_data) or {}
         processor = SourceProcessor(
-            sources=CfnginPackageSourcesDefinitionModel.parse_obj(
+            sources=CfnginPackageSourcesDefinitionModel.model_validate(
                 config.get("package_sources", {})
             ),
             cache_dir=Path(
@@ -436,7 +435,7 @@ def resolve_raw_data(
         return rendered
 
 
-class RunwayConfig(BaseConfig):
+class RunwayConfig(BaseConfig[RunwayConfigDefinitionModel]):
     """Python representation of a Runway config file."""
 
     ACCEPTED_NAMES = ["runway.yml", "runway.yaml"]
@@ -446,11 +445,9 @@ class RunwayConfig(BaseConfig):
     future: RunwayFutureDefinitionModel
     ignore_git_branch: bool
     runway_version: SpecifierSet | None
-    tests: list[RunwayTestDefinition[Any]]
+    tests: list[RunwayTestDefinition]
     variables: RunwayVariablesDefinition
 
-    _data: RunwayConfigDefinitionModel
-
     def __init__(self, data: RunwayConfigDefinitionModel, *, path: Path | None = None) -> None:
         """Instantiate class.
 
@@ -526,4 +523,4 @@ def parse_obj(cls, obj: Any, *, path: Path | None = None) -> RunwayConfig:
             path: Path to the file the object was parsed from.
 
         """
-        return cls(RunwayConfigDefinitionModel.parse_obj(obj), path=path)
+        return cls(RunwayConfigDefinitionModel.model_validate(obj), path=path)
diff --git a/runway/config/components/runway/__init__.py b/runway/config/components/runway/__init__.py
index e764ae0d9..2cdc0b95d 100644
--- a/runway/config/components/runway/__init__.py
+++ b/runway/config/components/runway/__init__.py
@@ -2,20 +2,12 @@
 
 from ._deployment_def import RunwayDeploymentDefinition
 from ._module_def import RunwayModuleDefinition
-from ._test_def import (
-    CfnLintRunwayTestDefinition,
-    RunwayTestDefinition,
-    ScriptRunwayTestDefinition,
-    YamlLintRunwayTestDefinition,
-)
+from ._test_def import RunwayTestDefinition
 from ._variables_def import RunwayVariablesDefinition
 
 __all__ = [
-    "CfnLintRunwayTestDefinition",
     "RunwayDeploymentDefinition",
     "RunwayModuleDefinition",
     "RunwayTestDefinition",
     "RunwayVariablesDefinition",
-    "ScriptRunwayTestDefinition",
-    "YamlLintRunwayTestDefinition",
 ]
diff --git a/runway/config/components/runway/_deployment_def.py b/runway/config/components/runway/_deployment_def.py
index d2ce0bba8..027629e33 100644
--- a/runway/config/components/runway/_deployment_def.py
+++ b/runway/config/components/runway/_deployment_def.py
@@ -7,14 +7,13 @@
 
 from ....exceptions import UnresolvedVariable
 from ....variables import Variable
-from ...models.runway import (
-    RunwayDeploymentDefinitionModel,
-    RunwayModuleDefinitionModel,
-)
+from ...models.runway import RunwayDeploymentDefinitionModel, RunwayModuleDefinitionModel
 from ._module_def import RunwayModuleDefinition
 from .base import ConfigComponentDefinition
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from ...models.base import ConfigProperty
     from ...models.runway import (
         RunwayAssumeRoleDefinitionModel,
@@ -25,7 +24,7 @@
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
-class RunwayDeploymentDefinition(ConfigComponentDefinition):
+class RunwayDeploymentDefinition(ConfigComponentDefinition[RunwayDeploymentDefinitionModel]):
     """Runway deployment definition."""
 
     account_alias: str | None
@@ -39,7 +38,6 @@ class RunwayDeploymentDefinition(ConfigComponentDefinition):
     parameters: dict[str, Any]
     regions: list[str]
 
-    _data: RunwayDeploymentDefinitionModel
     _pre_process_vars: tuple[str, ...] = (
         "account_alias",
         "account_id",
@@ -98,7 +96,9 @@ def modules(self, modules: list[RunwayModuleDefinition]) -> None:
         """
         if not all(isinstance(i, RunwayModuleDefinition) for i in modules):  # type: ignore
             raise TypeError("modules must be type list[RunwayModuleDefinition]")
-        self._data.modules = [RunwayModuleDefinitionModel.parse_obj(mod.data) for mod in modules]
+        self._data.modules = [
+            RunwayModuleDefinitionModel.model_validate(mod.data) for mod in modules
+        ]
 
     def reverse(self) -> None:
         """Reverse the order of modules and regions."""
@@ -126,7 +126,7 @@ def set_modules(
         sanitized: list[RunwayModuleDefinitionModel] = []
         for i, mod in enumerate(modules):
             if isinstance(mod, RunwayModuleDefinition):
-                sanitized.append(RunwayModuleDefinitionModel.parse_obj(mod.data))
+                sanitized.append(RunwayModuleDefinitionModel.model_validate(mod.data))
             elif isinstance(mod, RunwayModuleDefinitionModel):  # type: ignore
                 sanitized.append(mod)
             else:
@@ -152,23 +152,23 @@ def _register_variable(self, var_name: str, var_value: Any) -> None:
 
     @overload
     @classmethod
-    def parse_obj(cls, obj: list[dict[str, Any]]) -> list[RunwayDeploymentDefinition]: ...
+    def parse_obj(cls: type[Self], obj: list[dict[str, Any]]) -> list[Self]: ...
 
     @overload
     @classmethod
     def parse_obj(
-        cls,
+        cls: type[Self],
         obj: list[ConfigProperty] | set[ConfigProperty] | tuple[ConfigProperty, ...],
-    ) -> list[RunwayDeploymentDefinition]: ...
+    ) -> list[Self]: ...
 
     @overload
     @classmethod
-    def parse_obj(cls, obj: dict[str, Any] | ConfigProperty) -> RunwayDeploymentDefinition: ...
+    def parse_obj(cls: type[Self], obj: dict[str, Any] | ConfigProperty) -> Self: ...
 
     @classmethod
-    def parse_obj(  # type: ignore
-        cls, obj: Any
-    ) -> RunwayDeploymentDefinition | list[RunwayDeploymentDefinition]:
+    def parse_obj(  # pyright: ignore[reportIncompatibleMethodOverride]
+        cls: type[Self], obj: Any
+    ) -> Self | list[Self]:
         """Parse a python object into this class.
 
         Args:
@@ -177,4 +177,4 @@ def parse_obj(  # type: ignore
         """
         if isinstance(obj, (list, set, tuple)):
             return [cls(RunwayDeploymentDefinitionModel.parse_obj(o)) for o in obj]  # type: ignore
-        return cls(RunwayDeploymentDefinitionModel.parse_obj(obj))
+        return cls(RunwayDeploymentDefinitionModel.model_validate(obj))
diff --git a/runway/config/components/runway/_module_def.py b/runway/config/components/runway/_module_def.py
index b604f4849..19bbd3046 100644
--- a/runway/config/components/runway/_module_def.py
+++ b/runway/config/components/runway/_module_def.py
@@ -11,14 +11,12 @@
 if TYPE_CHECKING:
     from pathlib import Path
 
-    from ...models.runway import (
-        RunwayEnvironmentsType,
-        RunwayEnvVarsType,
-        RunwayModuleTypeTypeDef,
-    )
+    from typing_extensions import Self
+
+    from ...models.runway import RunwayEnvironmentsType, RunwayEnvVarsType, RunwayModuleTypeTypeDef
 
 
-class RunwayModuleDefinition(ConfigComponentDefinition):
+class RunwayModuleDefinition(ConfigComponentDefinition[RunwayModuleDefinitionModel]):
     """Runway module definition."""
 
     class_path: str | None
@@ -31,7 +29,6 @@ class RunwayModuleDefinition(ConfigComponentDefinition):
     tags: list[str]
     type: RunwayModuleTypeTypeDef | None
 
-    _data: RunwayModuleDefinitionModel
     _supports_vars: tuple[str, ...] = (
         "class_path",
         "env_vars",
@@ -53,7 +50,7 @@ def child_modules(self) -> list[RunwayModuleDefinition]:
     @child_modules.setter
     def child_modules(
         self,
-        modules: list[RunwayModuleDefinition | RunwayModuleDefinitionModel],  # type: ignore
+        modules: list[RunwayModuleDefinition | RunwayModuleDefinitionModel],
     ) -> None:
         """Set the value of the property.
 
@@ -69,7 +66,7 @@ def child_modules(
         sanitized: list[RunwayModuleDefinitionModel] = []
         for i, mod in enumerate(modules):
             if isinstance(mod, RunwayModuleDefinition):
-                sanitized.append(RunwayModuleDefinitionModel.parse_obj(mod.data))
+                sanitized.append(RunwayModuleDefinitionModel.model_validate(mod.data))
             elif isinstance(mod, RunwayModuleDefinitionModel):  # type: ignore
                 sanitized.append(mod)
             else:
@@ -110,11 +107,11 @@ def _register_variable(self, var_name: str, var_value: Any) -> None:
         )
 
     @classmethod
-    def parse_obj(cls, obj: Any) -> RunwayModuleDefinition:
+    def parse_obj(cls: type[Self], obj: object) -> Self:
         """Parse a python object into this class.
 
         Args:
             obj: The object to parse.
 
         """
-        return cls(RunwayModuleDefinitionModel.parse_obj(obj))
+        return cls(RunwayModuleDefinitionModel.model_validate(obj))
diff --git a/runway/config/components/runway/_variables_def.py b/runway/config/components/runway/_variables_def.py
index a8b14d218..ec8b25589 100644
--- a/runway/config/components/runway/_variables_def.py
+++ b/runway/config/components/runway/_variables_def.py
@@ -12,6 +12,8 @@
 from ...models.runway import RunwayVariablesDefinitionModel
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from ...._logging import RunwayLogger
 
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__.replace("._", ".")))
@@ -29,8 +31,8 @@ def __init__(self, data: RunwayVariablesDefinitionModel) -> None:
         """Instantiate class."""
         self._file_path = data.file_path
         self._sys_path = data.sys_path
-        data = RunwayVariablesDefinitionModel(**{**data.dict(), **self.__load_file()})
-        super().__init__(**data.dict(exclude={"file_path", "sys_path"}))
+        data = RunwayVariablesDefinitionModel(**{**data.model_dump(), **self.__load_file()})
+        super().__init__(**data.model_dump(exclude={"file_path", "sys_path"}))
 
     def __load_file(self) -> dict[str, Any]:
         """Load a variables file."""
@@ -55,11 +57,11 @@ def __load_file(self) -> dict[str, Any]:
         return {}
 
     @classmethod
-    def parse_obj(cls, obj: Any) -> RunwayVariablesDefinition:
+    def parse_obj(cls: type[Self], obj: Any) -> Self:
         """Parse a python object into this class.
 
         Args:
             obj: The object to parse.
 
         """
-        return cls(RunwayVariablesDefinitionModel.parse_obj(obj))
+        return cls(RunwayVariablesDefinitionModel.model_validate(obj))
diff --git a/runway/config/components/runway/base.py b/runway/config/components/runway/base.py
index 68973c99f..faf61e477 100644
--- a/runway/config/components/runway/base.py
+++ b/runway/config/components/runway/base.py
@@ -4,13 +4,15 @@
 
 import logging
 from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
 
 from ...._logging import PrefixAdaptor
 from ....exceptions import UnresolvedVariable
 from ....variables import Variable
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from ...._logging import RunwayLogger
     from ....context import RunwayContext
     from ...models.base import ConfigProperty
@@ -18,18 +20,20 @@
 
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__))
 
+_ConfigPropertyTypeVar = TypeVar("_ConfigPropertyTypeVar", bound="ConfigProperty")
+
 
-class ConfigComponentDefinition(ABC):
+class ConfigComponentDefinition(ABC, Generic[_ConfigPropertyTypeVar]):
     """Base class for Runway config components."""
 
-    _data: ConfigProperty
+    _data: _ConfigPropertyTypeVar
     _pre_process_vars: tuple[str, ...] = ()
     _supports_vars: tuple[str, ...] = ()
     _vars: dict[str, Variable] = {}
 
-    def __init__(self, data: ConfigProperty) -> None:
+    def __init__(self, data: _ConfigPropertyTypeVar) -> None:
         """Instantiate class."""
-        self._data = data.copy(deep=True)
+        self._data = data.model_copy(deep=True)
 
         self._vars = {}
         for var in self._supports_vars:
@@ -39,7 +43,7 @@ def __init__(self, data: ConfigProperty) -> None:
     @property
     def data(self) -> dict[str, Any]:
         """Return the underlying data as a dict."""
-        return self._data.dict()
+        return self._data.model_dump()
 
     def get(self, name: str, default: Any = None) -> None:
         """Get a value or return default if it is not found.
@@ -56,7 +60,7 @@ def resolve(
         context: RunwayContext,
         *,
         pre_process: bool = False,
-        variables: Optional[RunwayVariablesDefinition] = None,
+        variables: RunwayVariablesDefinition | None = None,
     ) -> None:
         """Resolve variables.
 
@@ -100,7 +104,7 @@ def _register_variable(self, var_name: str, var_value: Any) -> None:
 
     @classmethod
     @abstractmethod
-    def parse_obj(cls, obj: Any) -> ConfigComponentDefinition:
+    def parse_obj(cls: type[Self], obj: object) -> Self:
         """Parse a python object into this class.
 
         Args:
@@ -131,8 +135,8 @@ def __getattr__(self, name: str) -> Any:
         if name in self._vars and not self._vars[name].resolved:
             raise UnresolvedVariable(self._vars[name])
         if name in super().__getattribute__("_data"):
-            return super().__getattribute__("_data").__getattribute__(name)
-        raise AttributeError(f"{self.__class__.__name__} object has not attribute {name}")
+            return super().__getattribute__("_data").__getitem__(name)
+        raise AttributeError(f"{self.__class__.__name__} object has no attribute {name}")
 
     def __getitem__(self, name: str) -> Any:
         """Implement evaluation of self[name].
@@ -146,8 +150,8 @@ def __getitem__(self, name: str) -> Any:
         """
         try:
             return self.__getattr__(name)
-        except AttributeError:
-            raise KeyError(name) from None
+        except AttributeError as exc:
+            raise KeyError(name) from exc
 
     def __setattr__(self, name: str, value: Any) -> None:
         """Implement evaluation of self.name = value.
@@ -165,7 +169,7 @@ def __setattr__(self, name: str, value: Any) -> None:
         """
         prop = getattr(self.__class__, name, None)
         if isinstance(prop, property) and prop.fset:
-            prop.fset(self, value)  # type: ignore
+            prop.fset(self, value)
         elif isinstance(prop, property):
             raise AttributeError(f"setting {name} property is not supported")
         elif name.startswith("_") or name in dir(self):
diff --git a/runway/config/models/base.py b/runway/config/models/base.py
index 0a5d8e3d0..b7741eb35 100644
--- a/runway/config/models/base.py
+++ b/runway/config/models/base.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-import pydantic
+from pydantic import ConfigDict
 
 from ...utils import BaseModel
 
@@ -10,8 +10,8 @@
 class ConfigProperty(BaseModel):
     """Base class for Runway configuration properties."""
 
-    class Config(pydantic.BaseConfig):
-        """Model configuration."""
-
-        validate_all = True
-        validate_assignment = True
+    model_config = ConfigDict(
+        extra="ignore",
+        validate_default=True,
+        validate_assignment=True,
+    )
diff --git a/runway/config/models/cfngin/__init__.py b/runway/config/models/cfngin/__init__.py
index b02e8c89c..77e08d6ee 100644
--- a/runway/config/models/cfngin/__init__.py
+++ b/runway/config/models/cfngin/__init__.py
@@ -1,24 +1,14 @@
 """CFNgin config models."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
 import copy
 import locale
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Dict,
-    List,
-    Optional,
-    TypeVar,
-    Union,
-    cast,
-)
+from typing import TYPE_CHECKING, Annotated, Any, cast
 
 import yaml
-from pydantic import Extra, Field, Protocol, root_validator, validator
+from pydantic import ConfigDict, Field, field_validator, model_validator
 from typing_extensions import Literal
 
 from .. import utils
@@ -31,9 +21,8 @@
 )
 
 if TYPE_CHECKING:
-    from pydantic import BaseModel
-
-    Model = TypeVar("Model", bound=BaseModel)
+    from pydantic.config import JsonDict
+    from typing_extensions import Self
 
 __all__ = [
     "CfnginConfigDefinitionModel",
@@ -49,139 +38,195 @@
 class CfnginHookDefinitionModel(ConfigProperty):
     """Model for a CFNgin hook definition."""
 
-    args: Dict[str, Any] = Field(
-        default={},
-        title="Arguments",
-        description="Arguments that will be passed to the hook. (supports lookups)",
-    )
-    data_key: Optional[str] = Field(
-        default=None,
-        description="Key to use when storing the returned result of the hook.",
-    )
-    enabled: bool = Field(default=True, description="Whether the hook will be run.")
-    path: str = Field(..., description="Python importable path to the hook.")
-    required: bool = Field(
-        default=True,
-        description="Whether to continue execution if the hook results in an error.",
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "description": "Python classes or functions run before or after deploy/destroy actions."
+        },
+        title="CFNgin Hook Definition",
+        validate_default=True,
+        validate_assignment=True,
     )
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
-            "description": "Python classes or functions run before or after "
-            "deploy/destroy actions."
-        }
-        title = "CFNgin Hook Definition"
+    args: Annotated[
+        dict[str, Any],
+        Field(
+            title="Arguments",
+            description="Arguments that will be passed to the hook. (supports lookups)",
+        ),
+    ] = {}
+    data_key: Annotated[
+        str | None, Field(description="Key to use when storing the returned result of the hook.")
+    ] = None
+    enabled: Annotated[bool, Field(description="Whether the hook will be run.")] = True
+    path: Annotated[str, Field(description="Python importable path to the hook.")]
+    required: Annotated[
+        bool, Field(description="Whether to continue execution if the hook results in an error.")
+    ] = True
+
+
+@staticmethod
+def _stack_json_schema_extra(schema: JsonDict) -> None:
+    """Process the schema after it has been generated.
+
+    Schema is modified in place. Return value is ignored.
+
+    https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization
+
+    """
+    schema["description"] = "Define CloudFormation stacks using a Blueprint or Template."
+
+    # prevents a false error when defining stacks as a dict
+    if "required" in schema and isinstance(schema["required"], list):
+        schema["required"].remove("name")
+
+    # fields that can be bool or lookup
+    if "properties" in schema and isinstance(schema["properties"], dict):
+        properties = schema["properties"]
+        for field_name in ["enabled", "locked", "protected", "termination_protection"]:
+            if field_name in properties and isinstance(properties[field_name], dict):
+                field_schema = cast("JsonDict", properties[field_name])
+                field_schema.pop("type")
+                field_schema["anyOf"] = [
+                    {"type": "boolean"},
+                    {"type": "string", "pattern": utils.CFNGIN_LOOKUP_STRING_REGEX},
+                ]
 
 
 class CfnginStackDefinitionModel(ConfigProperty):
     """Model for a CFNgin stack definition."""
 
-    class_path: Optional[str] = Field(
-        default=None,
-        title="Blueprint Class Path",
-        description="Python importable path to a blueprint class.",
-    )
-    description: Optional[str] = Field(
-        default=None,
-        title="Stack Description",
-        description="A description that will be applied to the stack in CloudFormation.",
-    )
-    enabled: bool = Field(default=True, description="Whether the stack will be deployed.")
-    in_progress_behavior: Optional[Literal["wait"]] = Field(
-        default=None,
-        title="Stack In Progress Behavior",
-        description="The action to take when a stack's status is "
-        "CREATE_IN_PROGRESS or UPDATE_IN_PROGRESS when trying to update it.",
-    )
-    locked: bool = Field(default=False, description="Whether to limit updating of the stack.")
-    name: str = Field(..., title="Stack Name", description="Name of the stack.")
-    protected: bool = Field(
-        default=False,
-        description="Whether to force all updates to the stack to be performed interactively.",
-    )
-    required_by: List[str] = Field(
-        default=[], description="Array of stacks (by name) that require this stack."
-    )
-    requires: List[str] = Field(
-        default=[], description="Array of stacks (by name) that this stack requires."
-    )
-    stack_name: Optional[str] = Field(
-        default=None,
-        title="Explicit Stack Name",
-        description="Explicit name of the stack (namespace will still be prepended).",
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra=_stack_json_schema_extra,
+        title="CFNgin Stack Definition",
+        validate_default=True,
+        validate_assignment=True,
     )
-    stack_policy_path: Optional[Path] = Field(
-        default=None,
-        description="Path to a stack policy document that will be applied to the "
-        "CloudFormation stack.",
-    )
-    tags: Dict[str, Any] = Field(
-        default={}, description="Tags that will be applied to the CloudFormation stack."
-    )
-    template_path: Optional[Path] = Field(
-        default=None,
-        description="Path to a JSON or YAML formatted CloudFormation Template.",
-    )
-    termination_protection: bool = Field(
-        default=False,
-        description="Set the value of termination protection on the CloudFormation stack.",
-    )
-    timeout: Optional[int] = Field(
-        default=None,
-        description="The amount of time (in minutes) that can pass before the "
-        "Stack status becomes CREATE_FAILED.",
-    )
-    variables: Dict[str, Any] = Field(
-        default={},
-        description="Parameter values that will be passed to the "
-        "Blueprint/CloudFormation stack. (supports lookups)",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration options."""
 
-        extra = Extra.forbid
-        title = "CFNgin Stack Definition"
-
-        @staticmethod
-        def schema_extra(schema: dict[str, Any]) -> None:  # type: ignore
-            """Process the schema after it has been generated.
-
-            Schema is modified in place. Return value is ignored.
-
-            https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization
-
-            """
-            schema["description"] = "Define CloudFormation stacks using a Blueprint or Template."
-            # prevents a false error when defining stacks as a dict
-            schema.get("required", ["name"]).remove("name")
-
-            # fields that can be bool or lookup
-            for prop in ["enabled", "locked", "protected", "termination_protection"]:
-                schema["properties"][prop].pop("type")
-                schema["properties"][prop]["anyOf"] = [
-                    {"type": "boolean"},
-                    {"type": "string", "pattern": utils.CFNGIN_LOOKUP_STRING_REGEX},
-                ]
-
-    _resolve_path_fields = validator(  # type: ignore
-        "stack_policy_path", "template_path", allow_reuse=True
-    )(utils.resolve_path_field)
+    class_path: Annotated[
+        str | None,
+        Field(
+            title="Blueprint Class Path", description="Python importable path to a blueprint class."
+        ),
+    ] = None
+    """Python importable path to a blueprint class."""
+
+    description: Annotated[
+        str | None,
+        Field(
+            title="Stack Description",
+            description="A description that will be applied to the stack in CloudFormation.",
+        ),
+    ] = None
+    """A description that will be applied to the stack in CloudFormation."""
+
+    enabled: Annotated[bool, Field(description="Whether the stack will be deployed.")] = True
+    """Whether the stack will be deployed."""
+
+    in_progress_behavior: Annotated[
+        Literal["wait"] | None,
+        Field(
+            title="Stack In Progress Behavior",
+            description="The action to take when a stack's status is "
+            "CREATE_IN_PROGRESS or UPDATE_IN_PROGRESS when trying to update it.",
+        ),
+    ] = None
+    """The action to take when a Stack's status is ``CREATE_IN_PROGRESS`` or
+    ``UPDATE_IN_PROGRESS`` when trying to update it.
+
+    """
+
+    locked: Annotated[bool, Field(description="Whether to limit updating of the stack.")] = False
+    """Whether to limit updating of the stack."""
+
+    name: Annotated[str, Field(title="Stack Name", description="Name of the stack.")]
+    """Name of the stack."""
+
+    protected: Annotated[
+        bool,
+        Field(
+            description="Whether to force all updates to the stack to be performed interactively."
+        ),
+    ] = False
+    """Whether to force all updates to the stack to be performed interactively."""
+
+    required_by: Annotated[
+        list[str], Field(description="Array of stacks (by name) that require this stack.")
+    ] = []
+    """Array of stacks (by name) that require this stack."""
+
+    requires: Annotated[
+        list[str], Field(description="Array of stacks (by name) that this stack requires.")
+    ] = []
+    """Array of stacks (by name) that this stack requires."""
+
+    stack_name: Annotated[
+        str | None,
+        Field(
+            title="Explicit Stack Name",
+            description="Explicit name of the stack (namespace will still be prepended).",
+        ),
+    ] = None
+    """Explicit name of the stack (namespace will still be prepended)."""
+
+    stack_policy_path: Annotated[
+        Path | None,
+        Field(
+            description="Path to a stack policy document that will be applied to the CloudFormation stack."
+        ),
+    ] = None
+    """Path to a stack policy document that will be applied to the CloudFormation stack."""
+
+    tags: Annotated[
+        dict[str, Any], Field(description="Tags that will be applied to the CloudFormation stack.")
+    ] = {}
+    """Tags that will be applied to the CloudFormation stack."""
+
+    template_path: Annotated[
+        Path | None, Field(description="Path to a JSON or YAML formatted CloudFormation Template.")
+    ] = None
+    """Path to a JSON or YAML formatted CloudFormation Template."""
+
+    termination_protection: Annotated[
+        bool,
+        Field(description="Set the value of termination protection on the CloudFormation stack."),
+    ] = False
+    """Set the value of termination protection on the CloudFormation stack."""
+
+    timeout: Annotated[
+        int | None,
+        Field(
+            description="The amount of time (in minutes) that can pass before the Stack status becomes CREATE_FAILED."
+        ),
+    ] = None
+    """The amount of time (in minutes) that can pass before the Stack status becomes CREATE_FAILED."""
+
+    variables: Annotated[
+        dict[str, Any],
+        Field(
+            description="Parameter values that will be passed to the Blueprint/CloudFormation stack. (supports lookups)"
+        ),
+    ] = {}
+    """Parameter values that will be passed to the Blueprint/CloudFormation stack. (supports lookups)"""
+
+    _resolve_path_fields = field_validator("stack_policy_path", "template_path")(
+        utils.resolve_path_field
+    )
 
-    @root_validator(pre=True)  # type: ignore
-    def _validate_class_and_template(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_class_and_template(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Validate class_path and template_path are not both provided."""
         if values.get("class_path") and values.get("template_path"):
             raise ValueError("only one of class_path or template_path can be defined")
         return values
 
-    @root_validator(pre=True)  # type: ignore
-    def _validate_class_or_template(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_class_or_template(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Ensure that either class_path or template_path is defined."""
-        # if the stack is disabled or locked, it is ok that these are missing
+        # if the Stack is disabled or locked, it is ok that these are missing
         required = values.get("enabled", True) and not values.get("locked", False)
         if not values.get("class_path") and not values.get("template_path") and required:
             raise ValueError("either class_path or template_path must be defined")
@@ -191,120 +236,147 @@ def _validate_class_or_template(cls, values: dict[str, Any]) -> dict[str, Any]:
 class CfnginConfigDefinitionModel(ConfigProperty):
     """Model for a CFNgin config definition."""
 
-    cfngin_bucket: Optional[str] = Field(
-        default=None,
-        title="CFNgin Bucket",
-        description="Name of an AWS S3 bucket to use for caching CloudFormation templates. "
-        "Set as an empty string to disable caching.",
-    )
-    cfngin_bucket_region: Optional[str] = Field(
-        default=None,
-        title="CFNgin Bucket Region",
-        description="AWS Region where the CFNgin Bucket is located. "
-        "If not provided, the current region is used.",
-    )
-    cfngin_cache_dir: Optional[Path] = Field(
-        default=None,
-        title="CFNgin Cache Directory",
-        description="Path to a local directory that CFNgin will use for local caching.",
-    )
-    log_formats: Dict[str, str] = Field(  # TODO (kyle): create model
-        default={}, description="Customize log message formatting by log level."
-    )
-    lookups: Dict[str, str] = Field(
-        default={},
-        description="Mapping of custom lookup names to a python importable path "
-        "for the class that will be used to resolve the lookups.",
-    )
-    mappings: Dict[str, Dict[str, Dict[str, Any]]] = Field(
-        default={}, description="Mappings that will be appended to all stack templates."
-    )
-    namespace: str = Field(
-        ...,
-        description="The namespace used to prefix stack names to create separation "
-        "within an AWS account.",
-    )
-    namespace_delimiter: str = Field(
-        default="-",
-        description="Character used to separate the namespace and stack name "
-        "when the namespace is prepended.",
-    )
-    package_sources: CfnginPackageSourcesDefinitionModel = Field(
-        default=CfnginPackageSourcesDefinitionModel(),
-        description=CfnginPackageSourcesDefinitionModel.Config.schema_extra["description"],
-    )
-    persistent_graph_key: Optional[str] = Field(
-        default=None,
-        description="Key for an AWS S3 object used to track a graph of stacks "
-        "between executions.",
+    model_config = ConfigDict(
+        extra="ignore",
+        json_schema_extra={"description": "Configuration file for Runway's CFNgin."},
+        title="CFNgin Config File",
+        validate_default=True,
+        validate_assignment=True,
     )
-    post_deploy: Union[
-        List[CfnginHookDefinitionModel],  # final type after parsing
-        Dict[str, CfnginHookDefinitionModel],  # recommended when writing config
-    ] = Field(default=[], title="Post Deploy Hooks")
-    post_destroy: Union[
-        List[CfnginHookDefinitionModel],  # final type after parsing
-        Dict[str, CfnginHookDefinitionModel],  # recommended when writing config
-    ] = Field(default=[], title="Pre Destroy Hooks")
-    pre_deploy: Union[
-        List[CfnginHookDefinitionModel],  # final type after parsing
-        Dict[str, CfnginHookDefinitionModel],  # recommended when writing config
-    ] = Field(default=[], title="Pre Deploy Hooks")
-    pre_destroy: Union[
-        List[CfnginHookDefinitionModel],  # final type after parsing
-        Dict[str, CfnginHookDefinitionModel],  # recommended when writing config
-    ] = Field(default=[], title="Pre Destroy Hooks")
-    service_role: Optional[str] = Field(
-        default=None,
-        title="Service Role ARN",
-        description="Specify an IAM Role for CloudFormation to use.",
-    )
-    stacks: Union[
-        List[CfnginStackDefinitionModel],  # final type after parsing
-        Dict[str, CfnginStackDefinitionModel],  # recommended when writing config
-    ] = Field(
-        default=[],
-        description="Define CloudFormation stacks using a Blueprint or Template.",
-    )
-    sys_path: Optional[Path] = Field(
-        default=None,
-        title="sys.path",
-        description="Path to append to $PATH. This is also the root of relative paths.",
-    )
-    tags: Optional[Dict[str, str]] = Field(
-        default=None,  # None is significant here
-        description="Tags to try to apply to all resources created from this configuration file.",
-    )
-    template_indent: int = Field(
-        default=4,
-        description="Number of spaces per indentation level to use when "
-        "rendering/outputting CloudFormation templates.",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        schema_extra = {"description": "Configuration file for Runway's CFNgin."}
-        title = "CFNgin Config File"
 
-    _resolve_path_fields = validator(  # type: ignore
-        "cfngin_cache_dir", "sys_path", allow_reuse=True
-    )(utils.resolve_path_field)
-
-    @validator("post_deploy", "post_destroy", "pre_deploy", "pre_destroy", pre=True)  # type: ignore
+    cfngin_bucket: Annotated[
+        str | None,
+        Field(
+            title="CFNgin Bucket",
+            description="Name of an AWS S3 bucket to use for caching CloudFormation templates. "
+            "Set as an empty string to disable caching.",
+        ),
+    ] = None
+    cfngin_bucket_region: Annotated[
+        str | None,
+        Field(
+            title="CFNgin Bucket Region",
+            description="AWS Region where the CFNgin Bucket is located. "
+            "If not provided, the current region is used.",
+        ),
+    ] = None
+    cfngin_cache_dir: Annotated[
+        Path | None,
+        Field(
+            title="CFNgin Cache Directory",
+            description="Path to a local directory that CFNgin will use for local caching.",
+        ),
+    ] = None
+    log_formats: Annotated[  # TODO (kyle): create model
+        dict[str, str], Field(description="Customize log message formatting by log level.")
+    ] = {}
+    lookups: Annotated[
+        dict[str, str],
+        Field(
+            description="Mapping of custom lookup names to a python importable path "
+            "for the class that will be used to resolve the lookups.",
+        ),
+    ] = {}
+    mappings: Annotated[
+        dict[str, dict[str, dict[str, Any]]],
+        Field(description="Mappings that will be appended to all stack templates."),
+    ] = {}
+    namespace: Annotated[
+        str,
+        Field(
+            description="The namespace used to prefix stack names to create separation "
+            "within an AWS account.",
+        ),
+    ]
+    namespace_delimiter: Annotated[
+        str,
+        Field(
+            description="Character used to separate the namespace and stack name "
+            "when the namespace is prepended.",
+        ),
+    ] = "-"
+    package_sources: Annotated[
+        CfnginPackageSourcesDefinitionModel,
+        Field(
+            description="Map of additional package sources to include when "
+            "processing this configuration file.",
+        ),
+    ] = CfnginPackageSourcesDefinitionModel()
+    persistent_graph_key: Annotated[
+        str | None,
+        Field(
+            description="Key for an AWS S3 object used to track a graph of stacks "
+            "between executions.",
+        ),
+    ] = None
+    post_deploy: Annotated[
+        list[CfnginHookDefinitionModel] | dict[str, CfnginHookDefinitionModel],
+        Field(title="Post Deploy Hooks"),
+    ] = []
+    post_destroy: Annotated[
+        list[CfnginHookDefinitionModel] | dict[str, CfnginHookDefinitionModel],
+        Field(title="Pre Destroy Hooks"),
+    ] = []
+    pre_deploy: Annotated[
+        list[CfnginHookDefinitionModel] | dict[str, CfnginHookDefinitionModel],
+        Field(title="Pre Deploy Hooks"),
+    ] = []
+    pre_destroy: Annotated[
+        list[CfnginHookDefinitionModel] | dict[str, CfnginHookDefinitionModel],
+        Field(title="Pre Destroy Hooks"),
+    ] = []
+    service_role: Annotated[
+        str | None,
+        Field(
+            title="Service Role ARN",
+            description="Specify an IAM Role for CloudFormation to use.",
+        ),
+    ] = None
+    stacks: Annotated[
+        list[CfnginStackDefinitionModel] | dict[str, CfnginStackDefinitionModel],
+        Field(
+            description="Define CloudFormation stacks using a Blueprint or Template.",
+        ),
+    ] = []
+    sys_path: Annotated[
+        Path | None,
+        Field(
+            title="sys.path",
+            description="Path to append to $PATH. This is also the root of relative paths.",
+        ),
+    ] = None
+    tags: Annotated[
+        dict[str, str] | None,
+        Field(
+            description="Tags to try to apply to all resources created from this configuration file.",
+        ),
+    ] = None  # NOTE (kyle): `None` is significant here
+    template_indent: Annotated[
+        int,
+        Field(
+            description="Number of spaces per indentation level to use when "
+            "rendering/outputting CloudFormation templates.",
+        ),
+    ] = 4
+
+    _resolve_path_fields = field_validator("cfngin_cache_dir", "sys_path")(utils.resolve_path_field)
+
+    @field_validator("post_deploy", "post_destroy", "pre_deploy", "pre_destroy", mode="before")
+    @classmethod
     def _convert_hook_definitions(
-        cls, v: Union[dict[str, Any], list[dict[str, Any]]]  # noqa: N805
+        cls, v: dict[str, Any] | list[dict[str, Any]]
     ) -> list[dict[str, Any]]:
         """Convert hooks defined as a dict to a list."""
         if isinstance(v, list):
             return v
         return list(v.values())
 
-    @validator("stacks", pre=True)  # type: ignore
+    @field_validator("stacks", mode="before")
+    @classmethod
     def _convert_stack_definitions(
-        cls, v: Union[dict[str, Any], list[dict[str, Any]]]  # noqa: N805
+        cls, v: dict[str, Any] | list[dict[str, Any]]
     ) -> list[dict[str, Any]]:
-        """Convert stacks defined as a dict to a list."""
+        """Convert ``stacks`` defined as a dict to a list."""
         if isinstance(v, list):
             return v
         result: list[dict[str, Any]] = []
@@ -313,11 +385,12 @@ def _convert_stack_definitions(
             result.append(stack)
         return result
 
-    @validator("stacks")  # type: ignore
+    @field_validator("stacks")
+    @classmethod
     def _validate_unique_stack_names(
-        cls, stacks: list[CfnginStackDefinitionModel]  # noqa: N805
+        cls, stacks: list[CfnginStackDefinitionModel]
     ) -> list[CfnginStackDefinitionModel]:
-        """Validate that each stack has a unique name."""
+        """Validate that each Stack has a unique name."""
         stack_names = [stack.name for stack in stacks]
         if len(set(stack_names)) != len(stack_names):
             for i, name in enumerate(stack_names):
@@ -326,36 +399,12 @@ def _validate_unique_stack_names(
         return stacks
 
     @classmethod
-    def parse_file(
-        cls: type[Model],
-        path: Union[str, Path],
-        *,
-        content_type: str | None = None,
-        encoding: str = "utf8",
-        proto: Protocol | None = None,
-        allow_pickle: bool = False,
-    ) -> Model:
+    def parse_file(  # pyright: ignore[reportIncompatibleMethodOverride]
+        cls: type[Self], path: str | Path
+    ) -> Self:
         """Parse a file."""
-        return cast(
-            "Model",
-            cls.parse_raw(
-                Path(path).read_text(encoding=locale.getpreferredencoding(do_setlocale=False)),
-                content_type=content_type,  # type: ignore
-                encoding=encoding,
-                proto=proto,  # type: ignore
-                allow_pickle=allow_pickle,
-            ),
+        return cls.model_validate(
+            yaml.safe_load(
+                Path(path).read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
+            )
         )
-
-    @classmethod
-    def parse_raw(
-        cls: type[Model],
-        b: Union[bytes, str],
-        *,
-        content_type: str | None = None,  # noqa: ARG003
-        encoding: str = "utf8",  # noqa: ARG003
-        proto: Protocol | None = None,  # noqa: ARG003
-        allow_pickle: bool = False,  # noqa: ARG003
-    ) -> Model:
-        """Parse raw data."""
-        return cast("Model", cls.parse_obj(yaml.safe_load(b)))
diff --git a/runway/config/models/cfngin/_package_sources.py b/runway/config/models/cfngin/_package_sources.py
index 82ff9de82..19f175dcd 100644
--- a/runway/config/models/cfngin/_package_sources.py
+++ b/runway/config/models/cfngin/_package_sources.py
@@ -1,11 +1,10 @@
 """CFNgin package source models."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
-from typing import Any, Dict, List, Optional
+from typing import Annotated, Any
 
-from pydantic import Extra, Field, root_validator
+from pydantic import ConfigDict, Field, model_validator
 
 from ..base import ConfigProperty
 
@@ -25,38 +24,41 @@ class GitCfnginPackageSourceDefinitionModel(ConfigProperty):
 
     """
 
-    branch: Optional[str] = Field(
-        default=None, title="Git Branch", examples=["ENV-dev", "ENV-prod", "master"]
-    )
-    commit: Optional[str] = Field(default=None, title="Git Commit Hash")
-    configs: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source "
-        "for configuration that should be merged into the current configuration file.",
-    )
-    paths: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source to add to $PATH.",
-    )
-    tag: Optional[str] = Field(default=None, title="Git Tag", examples=["1.0.0", "v1.0.0"])
-    uri: str = Field(
-        ...,
-        title="Git Repository URI",
-        examples=["git@github.com:onicagroup/runway.git"],
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra: Dict[str, Any] = {
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
             "description": "Information about git repositories that should be included "
             "in the processing of this configuration file."
-        }
-        title = "CFNgin Git Repository Package Source Definition"
-
-    @root_validator  # type: ignore
-    def _validate_one_ref(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+        },
+        title="CFNgin Git Repository Package Source Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+    branch: Annotated[
+        str | None, Field(title="Git Branch", examples=["ENV-dev", "ENV-prod", "master"])
+    ] = None
+    commit: Annotated[str | None, Field(title="Git Commit Hash")] = None
+    configs: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source "
+            "for configuration that should be merged into the current configuration file."
+        ),
+    ] = []
+    paths: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source to add to $PATH."
+        ),
+    ] = []
+    tag: Annotated[str | None, Field(title="Git Tag", examples=["1.0.0", "v1.0.0"])] = None
+    uri: Annotated[
+        str, Field(title="Git Repository URI", examples=["git@github.com:onicagroup/runway.git"])
+    ]
+
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_one_ref(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Ensure that only one ref is defined."""
         ref_keys = ["branch", "commit", "tag"]
         count_ref_defs = sum(bool(values.get(i)) for i in ref_keys)
@@ -77,30 +79,37 @@ class LocalCfnginPackageSourceDefinitionModel(ConfigProperty):
 
     """
 
-    configs: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source "
-        "for configuration that should be merged into the current configuration file.",
-    )
-    paths: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source to add to $PATH.",
-    )
-    source: str = Field(
-        ...,
-        description="Path relative to the current configuration file that is the "
-        "root of the local package source.",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra: Dict[str, Any] = {
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
             "description": "Information about local directories that should be "
             "included in the processing of this configuration file."
-        }
-        title = "CFNgin Local Package Source Definition"
+        },
+        title="CFNgin Local Package Source Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    configs: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source "
+            "for configuration that should be merged into the current configuration file.",
+        ),
+    ] = []
+    paths: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source to add to $PATH."
+        ),
+    ] = []
+    source: Annotated[
+        str,
+        Field(
+            description="Path relative to the current configuration file that is the "
+            "root of the local package source."
+        ),
+    ]
 
 
 class S3CfnginPackageSourceDefinitionModel(ConfigProperty):
@@ -118,37 +127,43 @@ class S3CfnginPackageSourceDefinitionModel(ConfigProperty):
 
     """
 
-    bucket: str = Field(..., title="AWS S3 Bucket Name")
-    configs: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source "
-        "for configuration that should be merged into the current configuration file.",
-    )
-    key: str = Field(..., title="AWS S3 Object Key")
-    paths: List[str] = Field(
-        default=[],
-        description="Array of paths relative to the root of the package source to add to $PATH.",
-    )
-    requester_pays: bool = Field(
-        default=False,
-        description="Confirms that the requester knows that they will be charged "
-        "for the request.",
-    )
-    use_latest: bool = Field(
-        default=True,
-        description="Update the local copy if the last modified date in AWS S3 changes.",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra: Dict[str, Any] = {
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
             "description": "Information about a AWS S3 objects that should be "
             "downloaded, unzipped, and included in the processing of "
             "this configuration file."
-        }
-        title = "CFNgin S3 Package Source Definition"
+        },
+        title="CFNgin S3 Package Source Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    bucket: Annotated[str, Field(title="AWS S3 Bucket Name")]
+    configs: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source "
+            "for configuration that should be merged into the current configuration file.",
+        ),
+    ] = []
+    key: Annotated[str, Field(title="AWS S3 Object Key")]
+    paths: Annotated[
+        list[str],
+        Field(
+            description="Array of paths relative to the root of the package source to add to $PATH."
+        ),
+    ] = []
+    requester_pays: Annotated[
+        bool,
+        Field(
+            description="Confirms that the requester knows that they will be charged for the request."
+        ),
+    ] = False
+    use_latest: Annotated[
+        bool,
+        Field(description="Update the local copy if the last modified date in AWS S3 changes."),
+    ] = True
 
 
 class CfnginPackageSourcesDefinitionModel(ConfigProperty):
@@ -161,28 +176,32 @@ class CfnginPackageSourcesDefinitionModel(ConfigProperty):
 
     """
 
-    git: List[GitCfnginPackageSourceDefinitionModel] = Field(
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "description": "Map of additional package sources to include when "
+            "processing this configuration file."
+        },
+        title="CFNgin Package Sources Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    git: list[GitCfnginPackageSourceDefinitionModel] = Field(
         default=[],
         title="CFNgin Git Repository Package Source Definitions",
-        description=GitCfnginPackageSourceDefinitionModel.Config.schema_extra["description"],
+        description="Information about git repositories that should be included "
+        "in the processing of this configuration file.",
     )
-    local: List[LocalCfnginPackageSourceDefinitionModel] = Field(
+    local: list[LocalCfnginPackageSourceDefinitionModel] = Field(
         default=[],
         title="CFNgin Local Package Source Definitions",
-        description=LocalCfnginPackageSourceDefinitionModel.Config.schema_extra["description"],
+        description="Information about local directories that should be included "
+        "in the processing of this configuration file.",
     )
-    s3: List[S3CfnginPackageSourceDefinitionModel] = Field(
+    s3: list[S3CfnginPackageSourceDefinitionModel] = Field(
         default=[],
         title="CFNgin S3 Package Source Definitions",
-        description=S3CfnginPackageSourceDefinitionModel.Config.schema_extra["description"],
+        description="Information about a AWS S3 objects that should be "
+        "downloaded, unzipped, and included in the processing of this configuration file.",
     )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra: Dict[str, Any] = {
-            "description": "Map of additional package sources to include when "
-            "processing this configuration file."
-        }
-        title = "CFNgin Package Sources Definition"
diff --git a/runway/config/models/runway/__init__.py b/runway/config/models/runway/__init__.py
index 2ab88e3e0..671f96b3c 100644
--- a/runway/config/models/runway/__init__.py
+++ b/runway/config/models/runway/__init__.py
@@ -2,304 +2,253 @@
 
 from __future__ import annotations
 
-# ruff: noqa: UP006, UP035
 import locale
 import logging
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    Dict,
-    List,
-    Optional,
-    TypeVar,
-    Union,
-    cast,
-)
+from typing import TYPE_CHECKING, Annotated, Any, TypeVar, cast
 
 import yaml
 from packaging.specifiers import InvalidSpecifier, SpecifierSet
-from pydantic import Extra, Field, Protocol, root_validator, validator
-from typing_extensions import Literal
+from pydantic import (
+    ConfigDict,
+    Field,
+    GetCoreSchemaHandler,
+    GetJsonSchemaHandler,
+    field_serializer,
+    field_validator,
+    model_validator,
+)
+from pydantic_core import CoreSchema, core_schema
 
+from ....utils.pydantic_validators import LaxStr
 from .. import utils
 from ..base import ConfigProperty
 from ..utils import RUNWAY_LOOKUP_STRING_ERROR, RUNWAY_LOOKUP_STRING_REGEX
-from ._builtin_tests import (
-    CfnLintRunwayTestArgs,
-    CfnLintRunwayTestDefinitionModel,
-    RunwayTestDefinitionModel,
-    ScriptRunwayTestArgs,
-    ScriptRunwayTestDefinitionModel,
-    ValidRunwayTestTypeValues,
-    YamlLintRunwayTestDefinitionModel,
+from ._assume_role import RunwayAssumeRoleDefinitionModel
+from ._builtin_tests import RunwayTestDefinitionModel, ValidRunwayTestTypeValues
+from ._future import RunwayFutureDefinitionModel
+from ._module import RunwayModuleDefinitionModel
+from ._region import RunwayDeploymentRegionDefinitionModel
+from ._type_defs import (
+    RunwayEnvironmentsType,
+    RunwayEnvironmentsUnresolvedType,
+    RunwayEnvVarsType,
+    RunwayEnvVarsUnresolvedType,
+    RunwayModuleTypeTypeDef,
 )
+from ._variables import RunwayVariablesDefinitionModel
 
 if TYPE_CHECKING:
-    from collections.abc import Generator
-
     from pydantic import BaseModel
+    from pydantic.json_schema import JsonSchemaValue
+    from typing_extensions import Self
 
     Model = TypeVar("Model", bound=BaseModel)
 
 LOGGER = logging.getLogger(__name__)
 
-RunwayEnvironmentsType = Dict[str, Union[bool, List[str], str]]
-RunwayEnvironmentsUnresolvedType = Union[Dict[str, Union[bool, List[str], str]], str]
-RunwayEnvVarsType = Dict[str, Union[List[str], str]]
-RunwayEnvVarsUnresolvedType = Union[RunwayEnvVarsType, str]
-RunwayModuleTypeTypeDef = Literal[
-    "cdk", "cloudformation", "kubernetes", "serverless", "static", "terraform"
-]
 
 __all__ = [
-    "CfnLintRunwayTestArgs",
-    "CfnLintRunwayTestDefinitionModel",
     "RUNWAY_LOOKUP_STRING_ERROR",
     "RUNWAY_LOOKUP_STRING_REGEX",
     "RunwayAssumeRoleDefinitionModel",
     "RunwayConfigDefinitionModel",
     "RunwayDeploymentDefinitionModel",
     "RunwayDeploymentRegionDefinitionModel",
-    "RunwayEnvironmentsType",
-    "RunwayEnvironmentsUnresolvedType",
     "RunwayEnvVarsType",
     "RunwayEnvVarsUnresolvedType",
+    "RunwayEnvironmentsType",
+    "RunwayEnvironmentsUnresolvedType",
     "RunwayFutureDefinitionModel",
     "RunwayModuleDefinitionModel",
     "RunwayModuleTypeTypeDef",
     "RunwayTestDefinitionModel",
     "RunwayVariablesDefinitionModel",
     "RunwayVersionField",
-    "ScriptRunwayTestArgs",
-    "ScriptRunwayTestDefinitionModel",
     "ValidRunwayTestTypeValues",
-    "YamlLintRunwayTestDefinitionModel",
 ]
 
 
-class RunwayAssumeRoleDefinitionModel(ConfigProperty):
-    """Model for a Runway assume role definition."""
+def _deployment_json_schema_extra(schema: dict[str, Any]) -> None:
+    """Process the schema after it has been generated.
 
-    arn: Optional[str] = Field(
-        default=None,
-        title="IAM Role ARN",
-        description="The ARN of the AWS IAM role to be assumed. (supports lookups)",
-    )
-    duration: Union[int, str] = Field(
-        default=3600,
-        description="The duration, in seconds, of the role session. (supports lookups)",
-        ge=900,  # applies to int json schema only
-        le=43_200,  # applies to int json schema only
-        regex=RUNWAY_LOOKUP_STRING_REGEX,  # applies to str json schema only
-    )
-    post_deploy_env_revert: bool = Field(
-        default=False,
-        title="Post Deployment Environment Revert",
-        description="Revert the credentials stored in environment variables to "
-        "what they were prior to execution after the deployment finished processing. "
-        "(supports lookups)",
-    )
-    session_name: str = Field(
-        default="runway",
-        description="An identifier for the assumed role session. (supports lookups)",
-    )
+    Schema is modified in place. Return value is ignored.
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
+    https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization
 
-        extra = Extra.forbid
-        schema_extra: dict[str, Any] = {
-            "description": "Used to defined a role to assume while Runway is "
-            "processing each module.",
-            "examples": [
-                {"arn": "arn:aws:iam::123456789012:role/name"},
-                {
-                    "arn": "${var role_arn.${env DEPLOY_ENVIRONMENT}}",
-                    "duration": 9001,
-                    "post_deploy_env_revert": True,
-                    "session_name": "runway-example",
-                },
-            ],
-        }
-        title = "Runway Deployment.assume_role Definition"
-
-    @validator("arn")  # type: ignore
-    def _convert_arn_null_value(cls, v: Optional[str]) -> Optional[str]:  # noqa: N805
-        """Convert a "nul" string into type(None)."""
-        null_strings = ["null", "none", "undefined"]
-        return None if isinstance(v, str) and v.lower() in null_strings else v
-
-    @validator("duration", pre=True)  # type: ignore
-    def _validate_duration(cls, v: Union[int, str]) -> Union[int, str]:  # noqa: N805
-        """Validate duration is within the range allowed by AWS."""
-        if isinstance(v, str):
-            return v
-        if v < 900:
-            raise ValueError("duration must be greater than or equal to 900")
-        if v > 43_200:
-            raise ValueError("duration must be less than or equal to 43,200")
-        return v
-
-    _validate_string_is_lookup = validator("duration", allow_reuse=True, pre=True)(  # type: ignore
-        utils.validate_string_is_lookup
-    )
-
-
-class RunwayDeploymentRegionDefinitionModel(ConfigProperty):
-    """Model for a Runway deployment region definition."""
-
-    parallel: Union[List[str], str] = Field(
-        ...,
-        title="Parallel Regions",
-        description="An array of AWS Regions to process asynchronously. (supports lookups)",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra: Dict[str, Any] = {
-            "description": "Only supports 'parallel' field.",
-            "examples": [
-                {"parallel": ["us-east-1", "us-east-2"]},
-                {"parallel": "${var regions.${env DEPLOY_ENVIRONMENT}}"},
-            ],
-        }
-        title = "Runway Deployment.regions Definition"
-
-    _validate_string_is_lookup = validator("parallel", allow_reuse=True, pre=True)(  # type: ignore
-        utils.validate_string_is_lookup
-    )
+    """
+    schema["description"] = "A collection of modules, regions, and other configurations to deploy."
+    # modify schema to allow simple string or mapping definition for a module
+    module_ref = schema["properties"]["modules"]["items"].pop("$ref")
+    schema["properties"]["modules"]["items"]["anyOf"] = [
+        {"$ref": module_ref},
+        {"type": "string"},
+    ]
 
 
 class RunwayDeploymentDefinitionModel(ConfigProperty):
     """Model for a Runway deployment definition."""
 
-    account_alias: Optional[str] = Field(
-        default=None,
-        description="Used to verify the currently assumed role or credentials. "
-        "(supports lookups)",
-        examples=["example-alias", "${var alias.${env DEPLOY_ENVIRONMENT}}"],
-    )
-    account_id: Optional[str] = Field(
-        default=None,
-        description="Used to verify the currently assumed role or credentials. "
-        "(supports lookups)",
-        examples=["123456789012", "${var id.${env DEPLOY_ENVIRONMENT}}"],
-    )
-    assume_role: Union[str, RunwayAssumeRoleDefinitionModel] = Field(
-        default={},
-        description="Assume a role when processing the deployment. (supports lookups)",
-        examples=[
-            "arn:aws:iam::123456789012:role/name",
-            *cast("list[Any]", RunwayAssumeRoleDefinitionModel.Config.schema_extra["examples"]),
-        ],
-    )
-    env_vars: RunwayEnvVarsUnresolvedType = Field(
-        default={},
-        title="Environment Variables",
-        description="Additional variables to add to the environment when "
-        "processing the deployment. (supports lookups)",
-        examples=[
-            "${var env_vars.${env DEPLOY_ENVIRONMENT}}",
-            {
-                "EXAMPLE_VARIABLE": "value",
-                "KUBECONFIG": [".kube", "${env DEPLOY_ENVIRONMENT}", "config"],
-            },
-        ],
-    )
-    environments: RunwayEnvironmentsUnresolvedType = Field(
-        default={},
-        description="Explicitly enable/disable the deployment for a specific "
-        "deploy environment, AWS Account ID, and AWS Region combination. "
-        "Can also be set as a static boolean value. (supports lookups)",
-        examples=[
-            "${var envs.${env DEPLOY_ENVIRONMENT}}",
-            {"dev": "123456789012", "prod": "us-east-1"},
-            {"dev": True, "prod": False},
-            {"dev": ["us-east-1"], "prod": ["us-west-2", "ca-central-1"]},
-            {
-                "dev": ["123456789012/us-east-1", "123456789012/us-west-2"],
-                "prod": ["234567890123/us-east-1", "234567890123/us-west-2"],
-            },
-        ],
-    )
-    modules: List[RunwayModuleDefinitionModel] = Field(
-        ..., description="An array of modules to process as part of a deployment."
-    )
-    module_options: Union[Dict[str, Any], str] = Field(
-        default={},
-        description="Options that are passed directly to the modules within this deployment. "
-        "(supports lookups)",
-        examples=[
-            "${var sampleapp.options.${env DEPLOY_ENVIRONMENT}}",
-            {"some_option": "value"},
-        ],
-    )
-    name: str = Field(
-        default="unnamed_deployment",
-        description="The name of the deployment to be displayed in logs and the "
-        "interactive selection menu.",
-    )
-    parallel_regions: Union[List[str], str] = Field(
-        default=[],
-        description="An array of AWS Regions to process asynchronously. (supports lookups)",
-        examples=[
-            ["us-east-1", "us-west-2"],
-            "${var regions.${dev DEPLOY_ENVIRONMENT}}",
-        ],
-    )
-    parameters: Union[Dict[str, Any], str] = Field(
-        default={},
-        description="Used to pass variable values to modules in place of an "
-        "environment configuration file. (supports lookups)",
-        examples=[
-            {"namespace": "example-${env DEPLOY_ENVIRONMENT}"},
-            "${var sampleapp.parameters.${env DEPLOY_ENVIRONMENT}}",
-        ],
-    )
-    regions: Union[List[str], str] = Field(
-        default=[],
-        description="An array of AWS Regions to process this deployment in. (supports lookups)",
-        examples=[
-            ["us-east-1", "us-west-2"],
-            "${var regions.${dev DEPLOY_ENVIRONMENT}}",
-        ]
-        + RunwayDeploymentRegionDefinitionModel.Config.schema_extra["examples"],
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway Deployment Definition"
-
-        @staticmethod
-        def schema_extra(schema: dict[str, Any]) -> None:  # type: ignore
-            """Process the schema after it has been generated.
-
-            Schema is modified in place. Return value is ignored.
-
-            https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra=_deployment_json_schema_extra,
+        title="Runway Deployment Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    account_alias: Annotated[
+        str | None,
+        Field(
+            description="Used to verify the currently assumed role or credentials. "
+            "(supports lookups)",
+            examples=["example-alias", "${var alias.${env DEPLOY_ENVIRONMENT}}"],
+        ),
+    ] = None
+    """Used to verify the currently assumed role or credentials. (supports lookups)"""
+
+    account_id: Annotated[
+        str | None,
+        LaxStr,
+        Field(
+            description="Used to verify the currently assumed role or credentials. "
+            "(supports lookups)",
+            examples=["123456789012", "${var id.${env DEPLOY_ENVIRONMENT}}"],
+        ),
+    ] = None
+    """Used to verify the currently assumed role or credentials. (supports lookups)"""
+
+    assume_role: Annotated[
+        str | RunwayAssumeRoleDefinitionModel | None,
+        Field(
+            description="Assume a role when processing the deployment. (supports lookups)",
+            examples=[
+                "arn:aws:iam::123456789012:role/name",
+                *cast(
+                    "dict[str, list[str]]",
+                    RunwayAssumeRoleDefinitionModel.model_config.get("json_schema_extra", {}),
+                ).get("examples", []),
+            ],
+        ),
+    ] = RunwayAssumeRoleDefinitionModel()
+    """Assume a role when processing the deployment. (supports lookups)"""
+
+    env_vars: Annotated[
+        RunwayEnvVarsUnresolvedType,
+        Field(
+            title="Environment Variables",
+            description="Additional variables to add to the environment when "
+            "processing the deployment. (supports lookups)",
+            examples=[
+                "${var env_vars.${env DEPLOY_ENVIRONMENT}}",
+                {
+                    "EXAMPLE_VARIABLE": "value",
+                    "KUBECONFIG": [".kube", "${env DEPLOY_ENVIRONMENT}", "config"],
+                },
+            ],
+        ),
+    ] = {}
+    """Additional variables to add to the environment when processing the deployment. (supports lookups)"""
+
+    environments: Annotated[
+        RunwayEnvironmentsUnresolvedType,
+        Field(
+            description="Explicitly enable/disable the deployment for a specific "
+            "deploy environment, AWS Account ID, and AWS Region combination. "
+            "Can also be set as a static boolean value. (supports lookups)",
+            examples=[
+                "${var envs.${env DEPLOY_ENVIRONMENT}}",
+                {"dev": "123456789012", "prod": "us-east-1"},
+                {"dev": True, "prod": False},
+                {"dev": ["us-east-1"], "prod": ["us-west-2", "ca-central-1"]},
+                {
+                    "dev": ["123456789012/us-east-1", "123456789012/us-west-2"],
+                    "prod": ["234567890123/us-east-1", "234567890123/us-west-2"],
+                },
+            ],
+        ),
+    ] = {}
+    """Explicitly enable/disable the deployment for a specific deploy environment,
+    AWS Account ID, and AWS Region combination.
+    Can also be set as a static boolean value. (supports lookups)
+
+    """
+
+    modules: Annotated[
+        list[RunwayModuleDefinitionModel],
+        Field(description="An array of modules to process as part of a deployment."),
+    ]
+    """An array of modules to process as part of a deployment."""
+
+    module_options: Annotated[
+        dict[str, Any] | str,
+        Field(
+            description="Options that are passed directly to the modules within this deployment. "
+            "(supports lookups)",
+            examples=[
+                "${var sampleapp.options.${env DEPLOY_ENVIRONMENT}}",
+                {"some_option": "value"},
+            ],
+        ),
+    ] = {}
+    """Options that are passed directly to the modules within this deployment. (supports lookups)"""
+
+    name: Annotated[
+        str,
+        Field(
+            description="The name of the deployment to be displayed in logs and the "
+            "interactive selection menu.",
+        ),
+    ] = "unnamed_deployment"
+    """The name of the deployment to be displayed in logs and the interactive selection menu."""
+
+    parallel_regions: Annotated[
+        list[str] | str,
+        Field(
+            description="An array of AWS Regions to process asynchronously. (supports lookups)",
+            examples=[
+                ["us-east-1", "us-west-2"],
+                "${var regions.${dev DEPLOY_ENVIRONMENT}}",
+            ],
+        ),
+    ] = []
+    """An array of AWS Regions to process asynchronously. (supports lookups)"""
+
+    parameters: Annotated[
+        dict[str, Any] | str,
+        Field(
+            description="Used to pass variable values to modules in place of an "
+            "environment configuration file. (supports lookups)",
+            examples=[
+                {"namespace": "example-${env DEPLOY_ENVIRONMENT}"},
+                "${var sampleapp.parameters.${env DEPLOY_ENVIRONMENT}}",
+            ],
+        ),
+    ] = {}
+    """Used to pass variable values to modules in place of an environment configuration file. (supports lookups)"""
+
+    regions: Annotated[
+        list[str] | str,
+        Field(
+            description="An array of AWS Regions to process this deployment in. (supports lookups)",
+            examples=[
+                ["us-east-1", "us-west-2"],
+                "${var regions.${dev DEPLOY_ENVIRONMENT}}",
+                *cast(
+                    "dict[str, list[str]]",
+                    RunwayDeploymentRegionDefinitionModel.model_config.get("json_schema_extra", {}),
+                ).get("examples", []),
+            ],
+        ),
+    ] = []
+    """An array of AWS Regions to process this deployment in. (supports lookups)"""
 
-            """
-            schema["description"] = (
-                "A collection of modules, regions, and other configurations to deploy."
-            )
-            # modify schema to allow simple string or mapping definition for a module
-            module_ref = schema["properties"]["modules"]["items"].pop("$ref")
-            schema["properties"]["modules"]["items"]["anyOf"] = [
-                {"$ref": module_ref},
-                {"type": "string"},
-            ]
-
-    @root_validator(pre=True)  # type: ignore
-    def _convert_simple_module(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _convert_simple_module(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Convert simple modules to dicts."""
         modules = values.get("modules", [])
-        result: List[dict[str, Any]] = []
+        result: list[dict[str, Any]] = []
         for module in modules:
             if isinstance(module, str):
                 result.append({"path": module})
@@ -308,10 +257,11 @@ def _convert_simple_module(cls, values: dict[str, Any]) -> dict[str, Any]:  # no
         values["modules"] = result
         return values
 
-    @root_validator(pre=True)  # type: ignore
-    def _validate_regions(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_regions(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Validate & simplify regions."""
-        raw_regions = values.get("regions", [])
+        raw_regions: str | list[str] = values.get("regions", [])
         parallel_regions = values.get("parallel_regions", [])
         if all(isinstance(i, str) for i in [raw_regions, parallel_regions]):
             raise ValueError(
@@ -322,7 +272,7 @@ def _validate_regions(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N
         if isinstance(raw_regions, list):
             regions = raw_regions
         else:
-            regions = RunwayDeploymentRegionDefinitionModel.parse_obj(raw_regions)
+            regions = RunwayDeploymentRegionDefinitionModel.model_validate(raw_regions)
 
         if regions and parallel_regions:
             raise ValueError("only one of parallel_regions or regions can be defined")
@@ -334,222 +284,54 @@ def _validate_regions(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N
             values["parallel_regions"] = regions.parallel
         return values
 
-    _validate_string_is_lookup = validator(  # type: ignore
+    _validate_string_is_lookup = field_validator(
         "env_vars",
         "environments",
         "module_options",
         "parallel_regions",
         "parameters",
         "regions",
-        allow_reuse=True,
-        pre=True,
-    )(utils.validate_string_is_lookup)
-
-
-class RunwayFutureDefinitionModel(ConfigProperty):
-    """Model for the Runway future definition."""
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
-            "description": "Enable features/behaviors that will be become standard "
-            "ahead of their official release."
-        }
-        title = "Runway Future Definition"
-
-
-class RunwayModuleDefinitionModel(ConfigProperty):
-    """Model for a Runway module definition."""
-
-    class_path: Optional[str] = Field(
-        default=None,
-        description="Import path to a custom Runway module class. (supports lookups)",
-    )
-    env_vars: RunwayEnvVarsUnresolvedType = Field(
-        default={},
-        title="Environment Variables",
-        description="Additional variables to add to the environment when "
-        "processing the deployment. (supports lookups)",
-        examples=[
-            "${var env_vars.${env DEPLOY_ENVIRONMENT}}",
-            {
-                "EXAMPLE_VARIABLE": "value",
-                "KUBECONFIG": [".kube", "${env DEPLOY_ENVIRONMENT}", "config"],
-            },
-        ],
-    )
-    environments: RunwayEnvironmentsUnresolvedType = Field(
-        default={},
-        description="Explicitly enable/disable the deployment for a specific "
-        "deploy environment, AWS Account ID, and AWS Region combination. "
-        "Can also be set as a static boolean value. (supports lookups)",
-        examples=[
-            "${var envs.${env DEPLOY_ENVIRONMENT}}",
-            {"dev": "123456789012", "prod": "us-east-1"},
-            {"dev": True, "prod": False},
-            {"dev": ["us-east-1"], "prod": ["us-west-2", "ca-central-1"]},
-            {
-                "dev": ["123456789012/us-east-1", "123456789012/us-west-2"],
-                "prod": ["234567890123/us-east-1", "234567890123/us-west-2"],
-            },
-        ],
-    )
-    name: str = Field(
-        default="undefined",
-        description="The name of the module to be displayed in logs and the "
-        "interactive selection menu.",
-    )
-    options: Union[Dict[str, Any], str] = Field(
-        default={}, description="Module type specific options. (supports lookups)"
-    )
-    parameters: Union[Dict[str, Any], str] = Field(
-        default={},
-        description="Used to pass variable values to modules in place of an "
-        "environment configuration file. (supports lookups)",
-        examples=[
-            {"namespace": "example-${env DEPLOY_ENVIRONMENT}"},
-            "${var sampleapp.parameters.${env DEPLOY_ENVIRONMENT}}",
-        ],
-    )
-    path: Optional[Union[str, Path]] = Field(
-        default=None,
-        description="Directory (relative to the Runway config file) containing IaC. "
-        "(supports lookups)",
-        examples=["./", "sampleapp-${env DEPLOY_ENVIRONMENT}.cfn", "sampleapp.sls"],
-    )
-    tags: List[str] = Field(
-        default=[],
-        description="Array of values to categorize the module which can be used "
-        "with the CLI to quickly select a group of modules. "
-        "This field is only used by the `--tag` CLI option.",
-        examples=[["type:network", "app:sampleapp"]],
-    )
-    type: Optional[RunwayModuleTypeTypeDef] = None
-    # needs to be last
-    parallel: List[RunwayModuleDefinitionModel] = Field(
-        default=[],
-        description="Array of module definitions that can be executed asynchronously. "
-        "Incompatible with class_path, path, and type.",
-        examples=[[{"path": "sampleapp-01.cfn"}, {"path": "sampleapp-02.cfn"}]],
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
-            "description": "Defines a directory containing IaC, "
-            "the parameters to pass in during execution, "
-            "and any applicable options for the module type.",
-        }
-        title = "Runway Module Definition"
-        use_enum_values = True
-
-    @root_validator(pre=True)  # type: ignore
-    def _validate_name(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
-        """Validate module name."""
-        if "name" in values:
-            return values
-        if "parallel" in values:
-            values["name"] = "parallel_parent"
-            return values
-        if "path" in values:
-            values["name"] = Path(values["path"]).resolve().name
-            return values
-        return values
-
-    @root_validator(pre=True)  # type: ignore
-    def _validate_path(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
-        """Validate path and sets a default value if needed."""
-        if not values.get("path") and not values.get("parallel"):
-            values["path"] = Path.cwd()
-        return values
-
-    @validator("parallel", pre=True)  # type: ignore
-    def _validate_parallel(
-        cls, v: List[Union[dict[str, Any], str]], values: dict[str, Any]  # noqa: N805
-    ) -> List[dict[str, Any]]:
-        """Validate parallel."""
-        if v and values.get("path"):
-            raise ValueError("only one of parallel or path can be defined")
-        result: List[dict[str, Any]] = []
-        for mod in v:
-            if isinstance(mod, str):
-                result.append({"path": mod})
-            else:
-                result.append(mod)
-        return result
-
-    # TODO (kyle): add regex to schema
-    _validate_string_is_lookup = validator(  # type: ignore
-        "env_vars",
-        "environments",
-        "options",
-        "parameters",
-        allow_reuse=True,
-        pre=True,
+        mode="before",
     )(utils.validate_string_is_lookup)
 
 
-# https://pydantic-docs.helpmanual.io/usage/postponed_annotations/#self-referencing-models
-RunwayModuleDefinitionModel.update_forward_refs()
-
-
-class RunwayVariablesDefinitionModel(ConfigProperty):
-    """Model for a Runway variable definition."""
-
-    file_path: Optional[Path] = Field(
-        default=None,
-        title="Variables File Path",
-        description="Explicit path to a variables file that will be loaded and "
-        "merged with the variables defined here.",
-    )
-    sys_path: Path = Field(
-        default="./",
-        description="Directory to use as the root of a relative 'file_path'. "
-        "If not provided, the current working directory is used.",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.allow
-        schema_extra = {
-            "description": "A variable definitions for the Runway config file. "
-            "This is used to resolve the 'var' lookup.",
-        }
-        title = "Runway Variables Definition"
-
-    _convert_null_values = validator("*", allow_reuse=True)(  # type: ignore
-        utils.convert_null_values
-    )
-
-
 class RunwayVersionField(SpecifierSet):
     """Extends packaging.specifiers.SpecifierSet for use with pydantic."""
 
     @classmethod
-    def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
+    def __get_pydantic_core_schema__(
+        cls, source_type: Any, handler: GetCoreSchemaHandler
+    ) -> CoreSchema:
         """Yield one of more validators with will be called to validate the input.
 
         Each validator will receive, as input, the value returned from the previous validator.
 
         """
-        yield cls._convert_value
+        assert source_type is RunwayVersionField  # noqa: S101
+        return core_schema.no_info_before_validator_function(
+            cls._convert_value,
+            core_schema.any_schema(),
+            serialization=core_schema.plain_serializer_function_ser_schema(
+                str, info_arg=False, return_schema=core_schema.str_schema()
+            ),
+        )
 
     @classmethod
-    def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
+    def __get_pydantic_json_schema__(
+        cls, schema: core_schema.JsonSchema, handler: GetJsonSchemaHandler
+    ) -> JsonSchemaValue:  # cov: ignore
         """Mutate the field schema in place.
 
         This is only called when output JSON schema from a model.
 
         """
-        field_schema.update(type="string")  # cov: ignore
+        json_schema = handler(schema)
+        json_schema = handler.resolve_ref_schema(json_schema)
+        json_schema["type"] = "str"
+        return json_schema
 
     @classmethod
-    def _convert_value(cls, v: Union[str, SpecifierSet]) -> RunwayVersionField:
+    def _convert_value(cls, v: str | SpecifierSet) -> RunwayVersionField:
         """Convert runway_version string into SpecifierSet with some value handling.
 
         Args:
@@ -576,40 +358,63 @@ def _convert_value(cls, v: Union[str, SpecifierSet]) -> RunwayVersionField:
 class RunwayConfigDefinitionModel(ConfigProperty):
     """Runway configuration definition model."""
 
-    deployments: List[RunwayDeploymentDefinitionModel] = Field(
-        default=[], description="Array of Runway deployments definitions."
-    )
-    future: RunwayFutureDefinitionModel = RunwayFutureDefinitionModel()
-    ignore_git_branch: bool = Field(
-        default=False,
-        description="Optionally exclude the git branch name when determining the "
-        "current deploy environment.",
-    )
-    runway_version: Optional[RunwayVersionField] = Field(
-        default=None,
-        description="Define the versions of Runway that can be used with this "
-        "configuration file.",
-        examples=['"<2.0.0"', '"==1.14.0"', '">=1.14.0,<2.0.0"'],
-    )
-    tests: List[RunwayTestDefinitionModel] = Field(
-        default=[],
-        description="Array of Runway test definitions that are executed with the 'test' command.",
-    )
-    variables: RunwayVariablesDefinitionModel = RunwayVariablesDefinitionModel()
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
+    model_config = ConfigDict(
+        arbitrary_types_allowed=True,
+        json_schema_extra={
             "description": "Configuration file for use with Runway.",
-        }
-        title = "Runway Configuration File"
-        validate_all = True
-        validate_assignment = True
-
-    @root_validator(pre=True)  # type: ignore
-    def _add_deployment_names(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+        },
+        extra="forbid",
+        title="Runway Configuration File",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    deployments: Annotated[
+        list[RunwayDeploymentDefinitionModel],
+        Field(description="Array of Runway deployments definitions."),
+    ] = []
+    """Array of Runway deployments definitions."""
+
+    future: Annotated[
+        RunwayFutureDefinitionModel,
+        Field(description="Enable future features before they become default behavior."),
+    ] = RunwayFutureDefinitionModel()
+    """Enable future features before they become default behavior."""
+
+    ignore_git_branch: Annotated[
+        bool,
+        Field(
+            description="Optionally exclude the git branch name when determining the "
+            "current deploy environment.",
+        ),
+    ] = False
+    """Optionally exclude the git branch name when determining the current deploy environment."""
+
+    runway_version: Annotated[
+        RunwayVersionField | None,
+        Field(
+            description="Define the versions of Runway that can be used with this "
+            "configuration file.",
+            examples=['"<2.0.0"', '"==1.14.0"', '">=1.14.0,<2.0.0"'],
+        ),
+    ] = None
+    """Define the versions of Runway that can be used with this configuration file."""
+
+    tests: Annotated[
+        list[RunwayTestDefinitionModel],
+        Field(
+            description="Array of Runway test definitions that are executed with the 'test' command."
+        ),
+    ] = []
+
+    variables: Annotated[RunwayVariablesDefinitionModel, Field(description="Runway variables.")] = (
+        RunwayVariablesDefinitionModel()
+    )
+    """Runway variables."""
+
+    @model_validator(mode="before")
+    @classmethod
+    def _add_deployment_names(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Add names to deployments that are missing them."""
         deployments = values.get("deployments", [])
         for i, deployment in enumerate(deployments):
@@ -618,41 +423,18 @@ def _add_deployment_names(cls, values: dict[str, Any]) -> dict[str, Any]:  # noq
         values["deployments"] = deployments
         return values
 
+    @field_serializer("runway_version", when_used="json-unless-none")
+    def _serialize_runway_version(self, runway_version: RunwayVersionField, _info: Any) -> str:
+        """Serialize ``runway_version`` field when dumping to JSON."""
+        return str(runway_version)
+
     @classmethod
-    def parse_file(
-        cls: type[Model],
-        path: Union[str, Path],
-        *,
-        content_type: str | None = None,
-        encoding: str = "utf8",
-        proto: Protocol | None = None,
-        allow_pickle: bool = False,
-    ) -> Model:
+    def parse_file(  # pyright: ignore[reportIncompatibleMethodOverride]
+        cls: type[Self], path: str | Path
+    ) -> Self:
         """Parse a file."""
-        return cast(
-            "Model",
-            cls.parse_raw(
-                Path(path).read_text(encoding=locale.getpreferredencoding(do_setlocale=False)),
-                content_type=content_type,  # type: ignore
-                encoding=encoding,
-                proto=proto,  # type: ignore
-                allow_pickle=allow_pickle,
-            ),
+        return cls.model_validate(
+            yaml.safe_load(
+                Path(path).read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
+            )
         )
-
-    @classmethod
-    def parse_raw(
-        cls: type[Model],
-        b: Union[bytes, str],
-        *,
-        content_type: str | None = None,  # noqa: ARG003
-        encoding: str = "utf8",  # noqa: ARG003
-        proto: Protocol | None = None,  # noqa: ARG003
-        allow_pickle: bool = False,  # noqa: ARG003
-    ) -> Model:
-        """Parse raw data."""
-        return cast("Model", cls.parse_obj(yaml.safe_load(b)))
-
-
-# https://pydantic-docs.helpmanual.io/usage/postponed_annotations/#self-referencing-models
-RunwayDeploymentDefinitionModel.update_forward_refs()
diff --git a/runway/config/models/runway/_assume_role.py b/runway/config/models/runway/_assume_role.py
new file mode 100644
index 000000000..347db5ce0
--- /dev/null
+++ b/runway/config/models/runway/_assume_role.py
@@ -0,0 +1,97 @@
+"""Runway ``assume_role`` definition model."""
+
+from __future__ import annotations
+
+from typing import Annotated
+
+from pydantic import ConfigDict, Field, field_validator
+
+from .. import utils
+from ..base import ConfigProperty
+from ..utils import RUNWAY_LOOKUP_STRING_REGEX
+
+
+class RunwayAssumeRoleDefinitionModel(ConfigProperty):
+    """Used to defined a role to assume while Runway is processing each module."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "examples": [
+                {"arn": "arn:aws:iam::123456789012:role/name"},
+                {
+                    "arn": "${var role_arn.${env DEPLOY_ENVIRONMENT}}",
+                    "duration": 9001,
+                    "post_deploy_env_revert": True,
+                    "session_name": "runway-example",
+                },
+            ],
+        },
+        title="Runway Deployment.assume_role Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    arn: Annotated[
+        str | None,
+        Field(description="The ARN of the AWS IAM role to be assumed. (supports lookups)"),
+    ] = None
+    """The ARN of the AWS IAM role to be assumed. (supports lookups)"""
+
+    duration: (
+        Annotated[
+            int,
+            Field(
+                description="The duration, in seconds, of the role session. (supports lookups)",
+                ge=900,
+                le=43200,
+            ),
+        ]
+        | Annotated[str, Field(pattern=RUNWAY_LOOKUP_STRING_REGEX)]
+    ) = 3600
+    """The duration, in seconds, of the role session. (supports lookups)"""
+
+    post_deploy_env_revert: Annotated[
+        bool,
+        Field(
+            title="Post Deployment Environment Revert",
+            description="Revert the credentials stored in environment variables to "
+            "what they were prior to execution after the deployment finished processing. "
+            "(supports lookups)",
+        ),
+    ] = False
+    """Revert the credentials stored in environment variables to what they were
+    prior to execution after the deployment finished processing. (supports lookups)
+
+    """
+
+    session_name: Annotated[
+        str,
+        Field(
+            description="An identifier for the assumed role session. (supports lookups)",
+        ),
+    ] = "runway"
+    """An identifier for the assumed role session. (supports lookups)"""
+
+    @field_validator("arn")
+    @classmethod
+    def _convert_arn_null_value(cls, v: str | None) -> str | None:
+        """Convert a "nul" string into type(None)."""
+        null_strings = ["null", "none", "undefined"]
+        return None if isinstance(v, str) and v.lower() in null_strings else v
+
+    @field_validator("duration", mode="before")
+    @classmethod
+    def _validate_duration(cls, v: int | str) -> int | str:
+        """Validate duration is within the range allowed by AWS."""
+        if isinstance(v, str):
+            return v
+        if v < 900:
+            raise ValueError("duration must be greater than or equal to 900")
+        if v > 43_200:
+            raise ValueError("duration must be less than or equal to 43,200")
+        return v
+
+    _validate_string_is_lookup = field_validator("duration", mode="before")(
+        utils.validate_string_is_lookup
+    )
diff --git a/runway/config/models/runway/_future.py b/runway/config/models/runway/_future.py
new file mode 100644
index 000000000..813f2b62b
--- /dev/null
+++ b/runway/config/models/runway/_future.py
@@ -0,0 +1,20 @@
+"""Runway ``future`` definition model."""
+
+from pydantic import ConfigDict
+
+from ..base import ConfigProperty
+
+
+class RunwayFutureDefinitionModel(ConfigProperty):
+    """Enable features/behaviors that will be become standard ahead of their official release."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "description": "Enable features/behaviors that will be become standard "
+            "ahead of their official release."
+        },
+        title="Runway Future Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
diff --git a/runway/config/models/runway/_module.py b/runway/config/models/runway/_module.py
new file mode 100644
index 000000000..d384b0c5d
--- /dev/null
+++ b/runway/config/models/runway/_module.py
@@ -0,0 +1,200 @@
+"""Runway Module definition model."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Annotated, Any
+
+from pydantic import ConfigDict, Field, ValidationInfo, field_validator, model_validator
+
+from .. import utils
+from ..base import ConfigProperty
+from ._type_defs import (
+    RunwayEnvironmentsUnresolvedType,
+    RunwayEnvVarsUnresolvedType,
+    RunwayModuleTypeTypeDef,
+)
+
+
+class RunwayModuleDefinitionModel(ConfigProperty):
+    """Defines a directory containing IaC, parameters, and options for the module type."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "description": "Defines a directory containing IaC, "
+            "the parameters to pass in during execution, "
+            "and any applicable options for the module type.",
+        },
+        title="Runway Module Definition",
+        use_enum_values=True,
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    class_path: Annotated[
+        str | None,
+        Field(
+            description="Import path to a custom Runway module class. (supports lookups)",
+        ),
+    ] = None
+    """Import path to a custom Runway module class. (supports lookups)"""
+
+    env_vars: Annotated[
+        RunwayEnvVarsUnresolvedType,
+        Field(
+            title="Environment Variables",
+            description="Additional variables to add to the environment when "
+            "processing the deployment. (supports lookups)",
+            examples=[
+                "${var env_vars.${env DEPLOY_ENVIRONMENT}}",
+                {
+                    "EXAMPLE_VARIABLE": "value",
+                    "KUBECONFIG": [".kube", "${env DEPLOY_ENVIRONMENT}", "config"],
+                },
+            ],
+        ),
+    ] = {}
+    """Additional variables to add to the environment when processing the deployment. (supports lookups)"""
+
+    environments: Annotated[
+        RunwayEnvironmentsUnresolvedType,
+        Field(
+            description="Explicitly enable/disable the deployment for a specific "
+            "deploy environment, AWS Account ID, and AWS Region combination. "
+            "Can also be set as a static boolean value. (supports lookups)",
+            examples=[
+                "${var envs.${env DEPLOY_ENVIRONMENT}}",
+                {"dev": "123456789012", "prod": "us-east-1"},
+                {"dev": True, "prod": False},
+                {"dev": ["us-east-1"], "prod": ["us-west-2", "ca-central-1"]},
+                {
+                    "dev": ["123456789012/us-east-1", "123456789012/us-west-2"],
+                    "prod": ["234567890123/us-east-1", "234567890123/us-west-2"],
+                },
+            ],
+        ),
+    ] = {}
+    """Explicitly enable/disable the deployment for a specific deploy environment,
+    AWS Account ID, and AWS Region combination.
+    Can also be set as a static boolean value. (supports lookups)
+
+    """
+
+    name: Annotated[
+        str,
+        Field(
+            description="The name of the module to be displayed in logs and the "
+            "interactive selection menu."
+        ),
+    ] = "undefined"
+    """The name of the module to be displayed in logs and the interactive selection menu."""
+
+    options: Annotated[
+        dict[str, Any] | str, Field(description="Module type specific options. (supports lookups)")
+    ] = {}
+    """Module type specific options. (supports lookups)"""
+
+    parameters: Annotated[
+        dict[str, Any] | str,
+        Field(
+            description="Used to pass variable values to modules in place of an "
+            "environment configuration file. (supports lookups)",
+            examples=[
+                {"namespace": "example-${env DEPLOY_ENVIRONMENT}"},
+                "${var sampleapp.parameters.${env DEPLOY_ENVIRONMENT}}",
+            ],
+        ),
+    ] = {}
+    """Used to pass variable values to modules in place of an environment configuration file. (supports lookups)"""
+
+    path: Annotated[
+        Path | str | None,
+        Field(
+            description="Directory (relative to the Runway config file) containing IaC. "
+            "(supports lookups)",
+            examples=["./", "sampleapp-${env DEPLOY_ENVIRONMENT}.cfn", "sampleapp.sls"],
+        ),
+    ] = None
+    """Directory (relative to the Runway config file) containing IaC. (supports lookups)"""
+
+    tags: Annotated[
+        list[str],
+        Field(
+            description="Array of values to categorize the module which can be used "
+            "with the CLI to quickly select a group of modules. "
+            "This field is only used by the `--tag` CLI option.",
+            examples=[["type:network", "app:sampleapp"]],
+        ),
+    ] = []
+    """Array of values to categorize the module which can be used with the CLI to
+    quickly select a group of modules.
+
+    This field is only used by the ``--tag`` CLI option.
+
+    """
+
+    type: Annotated[
+        RunwayModuleTypeTypeDef | None,
+        Field(
+            description="Explicitly define the module type. If not provided, this will be inferred."
+        ),
+    ] = None
+    """Explicitly define the module type. If not provided, this will be inferred."""
+
+    # needs to be last
+    parallel: Annotated[
+        list[RunwayModuleDefinitionModel],
+        Field(
+            description="Array of module definitions that can be executed asynchronously. "
+            "Incompatible with class_path, path, and type.",
+            examples=[[{"path": "sampleapp-01.cfn"}, {"path": "sampleapp-02.cfn"}]],
+        ),
+    ] = []
+    """List of module definitions that can be executed asynchronously.
+    Incompatible with class_path, path, and type.
+
+    """
+
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_name(cls, values: dict[str, Any]) -> dict[str, Any]:
+        """Validate module name."""
+        if "name" in values:
+            return values
+        if "parallel" in values:
+            values["name"] = "parallel_parent"
+            return values
+        if "path" in values and values:
+            values["name"] = Path(values["path"]).resolve().name
+            return values
+        return values
+
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_path(cls, values: dict[str, Any]) -> dict[str, Any]:
+        """Validate path and sets a default value if needed."""
+        if not values.get("path") and not values.get("parallel"):
+            values["path"] = Path.cwd()
+        return values
+
+    @field_validator("parallel", mode="before")
+    @classmethod
+    def _validate_parallel(
+        cls, v: list[dict[str, Any] | str], info: ValidationInfo
+    ) -> list[dict[str, Any]]:
+        """Validate parallel."""
+        if v and info.data.get("path"):
+            raise ValueError("only one of parallel or path can be defined")
+        result: list[dict[str, Any]] = []
+        for mod in v:
+            if isinstance(mod, str):
+                result.append({"path": mod})
+            else:
+                result.append(mod)
+        return result
+
+    # TODO(kyle): add regex to schema
+    _validate_string_is_lookup = field_validator(
+        "env_vars", "environments", "options", "parameters", mode="before"
+    )(utils.validate_string_is_lookup)
diff --git a/runway/config/models/runway/_region.py b/runway/config/models/runway/_region.py
new file mode 100644
index 000000000..43bff06c8
--- /dev/null
+++ b/runway/config/models/runway/_region.py
@@ -0,0 +1,40 @@
+"""Runway deployment ``region`` definition model."""
+
+from __future__ import annotations
+
+from typing import Annotated
+
+from pydantic import ConfigDict, Field, field_validator
+
+from .. import utils
+from ..base import ConfigProperty
+
+
+class RunwayDeploymentRegionDefinitionModel(ConfigProperty):
+    """Only supports ``parallel`` field."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        json_schema_extra={
+            "examples": [
+                {"parallel": ["us-east-1", "us-east-2"]},
+                {"parallel": "${var regions.${env DEPLOY_ENVIRONMENT}}"},
+            ],
+        },
+        title="Runway Deployment.regions Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    parallel: Annotated[
+        list[str] | str,
+        Field(
+            title="Parallel Regions",
+            description="An array of AWS Regions to process asynchronously. (supports lookups)",
+        ),
+    ]
+    """A list of AWS Regions to process asynchronously. (supports lookups)"""
+
+    _validate_string_is_lookup = field_validator("parallel", mode="before")(
+        utils.validate_string_is_lookup
+    )
diff --git a/runway/config/models/runway/_type_defs.py b/runway/config/models/runway/_type_defs.py
new file mode 100644
index 000000000..45370bfea
--- /dev/null
+++ b/runway/config/models/runway/_type_defs.py
@@ -0,0 +1,16 @@
+"""Type definitions."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Literal, Union
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+RunwayEnvironmentsType: TypeAlias = dict[str, Union[bool, list[str], str]]
+RunwayEnvironmentsUnresolvedType: TypeAlias = Union[dict[str, Union[bool, list[str], str]], str]
+RunwayEnvVarsType: TypeAlias = dict[str, Union[list[str], str]]
+RunwayEnvVarsUnresolvedType: TypeAlias = Union[RunwayEnvVarsType, str]
+RunwayModuleTypeTypeDef: TypeAlias = Literal[
+    "cdk", "cloudformation", "kubernetes", "serverless", "static", "terraform"
+]
diff --git a/runway/config/models/runway/_variables.py b/runway/config/models/runway/_variables.py
new file mode 100644
index 000000000..bc461f087
--- /dev/null
+++ b/runway/config/models/runway/_variables.py
@@ -0,0 +1,54 @@
+"""Runway ``variables`` definition model."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Annotated
+
+from pydantic import ConfigDict, Field, field_validator
+
+from .. import utils
+from ..base import ConfigProperty
+
+
+class RunwayVariablesDefinitionModel(ConfigProperty):
+    """A variable definitions for the Runway config file.
+
+    This is used to resolve the ``var`` lookup.
+
+    """
+
+    model_config = ConfigDict(
+        extra="allow",
+        json_schema_extra={
+            "description": "A variable definitions for the Runway config file. "
+            "This is used to resolve the 'var' lookup.",
+        },
+        title="Runway Variables Definition",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    file_path: Annotated[
+        Path | None,
+        Field(
+            title="Variables File Path",
+            description="Explicit path to a variables file that will be loaded and "
+            "merged with the variables defined here.",
+        ),
+    ] = None
+    """Explicit path to a variables file that will be loaded and merged with the variables defined here."""
+
+    sys_path: Annotated[
+        Path,
+        Field(
+            description="Directory to use as the root of a relative 'file_path'. "
+            "If not provided, the current working directory is used.",
+        ),
+    ] = "./"  # pyright: ignore[reportAssignmentType]
+    """Directory to use as the root of a relative 'file_path'.
+    If not provided, the current working directory is used.
+
+    """
+
+    _convert_null_values = field_validator("*")(utils.convert_null_values)
diff --git a/runway/config/models/runway/options/cdk.py b/runway/config/models/runway/options/cdk.py
index b27db65e6..0a04254ba 100644
--- a/runway/config/models/runway/options/cdk.py
+++ b/runway/config/models/runway/options/cdk.py
@@ -1,11 +1,8 @@
 """Runway AWS Cloud Development Kit Module options."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
-from typing import List
-
-from pydantic import Extra
+from pydantic import ConfigDict
 
 from ...base import ConfigProperty
 
@@ -13,11 +10,12 @@
 class RunwayCdkModuleOptionsDataModel(ConfigProperty):
     """Model for Runway AWS Cloud Development Kit Module options."""
 
-    build_steps: List[str] = []
-    skip_npm_ci: bool = False
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
+    model_config = ConfigDict(
+        extra="ignore",
+        title="Runway AWS Cloud Development Kit Module options",
+        validate_default=True,
+        validate_assignment=True,
+    )
 
-        extra = Extra.ignore
-        title = "Runway AWS Cloud Development Kit Module options."
+    build_steps: list[str] = []
+    skip_npm_ci: bool = False
diff --git a/runway/config/models/runway/options/k8s.py b/runway/config/models/runway/options/k8s.py
index e83128f20..6db7b792a 100644
--- a/runway/config/models/runway/options/k8s.py
+++ b/runway/config/models/runway/options/k8s.py
@@ -3,9 +3,8 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import Optional
 
-from pydantic import Extra
+from pydantic import ConfigDict
 
 from ...base import ConfigProperty
 
@@ -13,11 +12,12 @@
 class RunwayK8sModuleOptionsDataModel(ConfigProperty):
     """Model for Runway Kubernetes Module options."""
 
-    kubectl_version: Optional[str] = None
-    overlay_path: Optional[Path] = None
+    model_config = ConfigDict(
+        extra="ignore",
+        title="Runway Kubernetes Module options",
+        validate_default=True,
+        validate_assignment=True,
+    )
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.ignore
-        title = "Runway Kubernetes Module options."
+    kubectl_version: str | None = None
+    overlay_path: Path | None = None
diff --git a/runway/config/models/runway/options/serverless.py b/runway/config/models/runway/options/serverless.py
index 02567a7db..c9b418e7e 100644
--- a/runway/config/models/runway/options/serverless.py
+++ b/runway/config/models/runway/options/serverless.py
@@ -1,11 +1,10 @@
 """Runway Serverless Framework Module options."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
-from typing import Any, Dict, List, Optional
+from typing import Any
 
-from pydantic import Extra
+from pydantic import ConfigDict
 
 from ...base import ConfigProperty
 
@@ -13,31 +12,33 @@
 class RunwayServerlessPromotezipOptionDataModel(ConfigProperty):
     """Model for Runway Serverless module promotezip option."""
 
-    bucketname: Optional[str] = None
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway Serverless Framework Module promotezip option",
+        validate_default=True,
+        validate_assignment=True,
+    )
 
-        extra = Extra.forbid
-        title = "Runway Serverless Framework Module promotezip option"
+    bucketname: str | None = None
 
     def __bool__(self) -> bool:
         """Evaluate the boolean value of the object instance."""
-        return bool(self.dict(exclude_none=True))
+        return bool(self.model_dump(exclude_none=True))
 
 
 class RunwayServerlessModuleOptionsDataModel(ConfigProperty):
     """Model for Runway Serverless Framework Module options."""
 
-    args: List[str] = []
-    extend_serverless_yml: Dict[str, Any] = {}
+    model_config = ConfigDict(
+        extra="ignore",
+        title="Runway Serverless Framework Module options",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    args: list[str] = []
+    extend_serverless_yml: dict[str, Any] = {}
     promotezip: RunwayServerlessPromotezipOptionDataModel = (
         RunwayServerlessPromotezipOptionDataModel()
     )
     skip_npm_ci: bool = False
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.ignore
-        title = "Runway Serverless Framework Module options"
diff --git a/runway/config/models/runway/options/terraform.py b/runway/config/models/runway/options/terraform.py
index f8beb6468..97072c6e9 100644
--- a/runway/config/models/runway/options/terraform.py
+++ b/runway/config/models/runway/options/terraform.py
@@ -1,11 +1,10 @@
 """Runway Terraform Module options."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
-from typing import List, Optional, Union
+from typing import Annotated
 
-from pydantic import Extra, Field, validator
+from pydantic import ConfigDict, Field, field_validator
 
 from ...base import ConfigProperty
 
@@ -13,59 +12,62 @@
 class RunwayTerraformArgsDataModel(ConfigProperty):
     """Model for Runway Terraform Module args option."""
 
-    apply: List[str] = []
-    init: List[str] = []
-    plan: List[str] = []
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway Terraform Module args option",
+        validate_default=True,
+        validate_assignment=True,
+    )
 
-        extra = Extra.forbid
-        title = "Runway Terraform Module args option"
+    apply: list[str] = []
+    init: list[str] = []
+    plan: list[str] = []
 
 
 class RunwayTerraformBackendConfigDataModel(ConfigProperty):
     """Model for Runway Terraform Module terraform_backend_config option."""
 
-    bucket: Optional[str] = None
-    dynamodb_table: Optional[str] = None
-    region: Optional[str] = None
-    workspace_key_prefix: Optional[str] = None
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway Terraform Module terraform_backend_config option",
+        validate_default=True,
+        validate_assignment=True,
+    )
 
-        extra = Extra.forbid
-        title = "Runway Terraform Module terraform_backend_config option"
+    bucket: str | None = None
+    dynamodb_table: str | None = None
+    region: str | None = None
+    workspace_key_prefix: str | None = None
 
     def __bool__(self) -> bool:
         """Evaluate the boolean value of the object instance."""
-        data = self.dict(exclude_none=True)
+        data = self.model_dump(exclude_none=True)
         return "bucket" in data or "dynamodb_table" in data
 
 
 class RunwayTerraformModuleOptionsDataModel(ConfigProperty):
     """Model for Runway Terraform Module options."""
 
+    model_config = ConfigDict(
+        extra="ignore",
+        title="Runway Terraform Module options",
+        populate_by_name=True,
+        validate_default=True,
+        validate_assignment=True,
+    )
+
     args: RunwayTerraformArgsDataModel = RunwayTerraformArgsDataModel()
     backend_config: RunwayTerraformBackendConfigDataModel = Field(
         default=RunwayTerraformBackendConfigDataModel(),
         alias="terraform_backend_config",
     )
-    version: Optional[str] = Field(default=None, alias="terraform_version")
-    workspace: Optional[str] = Field(default=None, alias="terraform_workspace")
-    write_auto_tfvars: bool = Field(default=False, alias="terraform_write_auto_tfvars")
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.ignore
-        title = "Runway Terraform Module options"
+    version: Annotated[str | None, Field(alias="terraform_version")] = None
+    workspace: Annotated[str | None, Field(alias="terraform_workspace")] = None
+    write_auto_tfvars: Annotated[bool, Field(alias="terraform_write_auto_tfvars")] = False
 
-    @validator("args", pre=True)  # type: ignore
-    def _convert_args(
-        cls, v: Union[list[str], dict[str, list[str]]]  # noqa: N805
-    ) -> dict[str, list[str]]:
+    @field_validator("args", mode="before")
+    @classmethod
+    def _convert_args(cls, v: list[str] | dict[str, list[str]]) -> dict[str, list[str]]:
         """Convert args from list to dict."""
         if isinstance(v, list):
             return {"apply": v}
diff --git a/runway/config/models/utils.py b/runway/config/models/utils.py
index fc774a178..a65fdd8b3 100644
--- a/runway/config/models/utils.py
+++ b/runway/config/models/utils.py
@@ -8,9 +8,9 @@
 if TYPE_CHECKING:
     from pathlib import Path
 
-CFNGIN_LOOKUP_STRING_REGEX = r"^\${.*}$"
+CFNGIN_LOOKUP_STRING_REGEX = r"^\$\{.+\}$"
 RUNWAY_LOOKUP_STRING_ERROR = ValueError("field can only be a string if it's a lookup")
-RUNWAY_LOOKUP_STRING_REGEX = r"^\${.*}$"
+RUNWAY_LOOKUP_STRING_REGEX = r"^\$\{.+\}$"
 
 
 def convert_null_values(v: Any) -> Any:
diff --git a/runway/context/_base.py b/runway/context/_base.py
index 992706b33..0327a6814 100644
--- a/runway/context/_base.py
+++ b/runway/context/_base.py
@@ -64,7 +64,9 @@ def __init__(
     def boto3_credentials(self) -> Boto3CredentialsTypeDef:
         """Return a dict of boto3 credentials."""
         return Boto3CredentialsTypeDef(
-            **{key.lower(): value for key, value in self.current_aws_creds.items()}
+            **{
+                key.lower(): value for key, value in self.current_aws_creds.items()
+            }  # pyright: ignore[reportArgumentType]
         )
 
     @property
diff --git a/runway/context/_cfngin.py b/runway/context/_cfngin.py
index f4f60a122..73745956e 100644
--- a/runway/context/_cfngin.py
+++ b/runway/context/_cfngin.py
@@ -397,8 +397,8 @@ def lock_persistent_graph(self, lock_code: str) -> None:
                 'locked persistent graph "%s" with lock ID "%s"',
                 "/".join(
                     [
-                        self.persistent_graph_location["Bucket"],
-                        self.persistent_graph_location["Key"],
+                        self.persistent_graph_location.get("Bucket", "unknown"),
+                        self.persistent_graph_location.get("Key", "unknown"),
                     ]
                 ),
                 lock_code,
diff --git a/runway/context/_runway.py b/runway/context/_runway.py
index 8758527de..8f360dd54 100644
--- a/runway/context/_runway.py
+++ b/runway/context/_runway.py
@@ -65,7 +65,7 @@ def no_color(self) -> bool:
         """
         colorize = self.env.vars.get("RUNWAY_COLORIZE")  # explicitly enable/disable
         try:
-            if isinstance(colorize, bool):  # type: ignore
+            if isinstance(colorize, bool):
                 # catch False
                 return not colorize
             if colorize and isinstance(colorize, str):  # type: ignore
diff --git a/runway/core/components/_deploy_environment.py b/runway/core/components/_deploy_environment.py
index 0097de7e7..af079f986 100644
--- a/runway/core/components/_deploy_environment.py
+++ b/runway/core/components/_deploy_environment.py
@@ -7,7 +7,7 @@
 import os
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import click
 
@@ -32,19 +32,19 @@
 class DeployEnvironment(DelCachedPropMixin):
     """Runway deploy environment."""
 
-    __name: Optional[str]
+    __name: str | None
     _ignore_git_branch: bool
 
-    name_derived_from: Optional[str]
+    name_derived_from: str | None
     root_dir: Path
 
     def __init__(
         self,
         *,
-        environ: Optional[dict[str, str]] = None,
-        explicit_name: Optional[str] = None,
+        environ: dict[str, str] | None = None,
+        explicit_name: str | None = None,
         ignore_git_branch: bool = False,
-        root_dir: Optional[Path] = None,
+        root_dir: Path | None = None,
     ) -> None:
         """Instantiate class.
 
@@ -70,7 +70,7 @@ def aws_credentials(self) -> EnvVarsAwsCredentialsTypeDef:
         )
 
     @property
-    def aws_profile(self) -> Optional[str]:
+    def aws_profile(self) -> str | None:
         """Get AWS profile from environment variables."""
         return self.vars.get("AWS_PROFILE")
 
@@ -90,7 +90,7 @@ def aws_region(self, region: str) -> None:
         self._update_vars({"AWS_DEFAULT_REGION": region, "AWS_REGION": region})
 
     @cached_property
-    def branch_name(self) -> Optional[str]:
+    def branch_name(self) -> str | None:
         """Git branch name."""
         if isinstance(git, type):
             LOGGER.debug(
@@ -100,9 +100,7 @@ def branch_name(self) -> Optional[str]:
             return None
         try:
             LOGGER.debug("getting git branch name...")
-            return git.Repo(  # type: ignore
-                str(self.root_dir), search_parent_directories=True
-            ).active_branch.name
+            return git.Repo(str(self.root_dir), search_parent_directories=True).active_branch.name
         except InvalidGitRepositoryError:
             return None
         except TypeError:
@@ -327,7 +325,7 @@ def log_name(self) -> None:
                 "override via the DEPLOY_ENVIRONMENT environment variable"
             )
 
-    def _parse_branch_name(self) -> Optional[str]:
+    def _parse_branch_name(self) -> str | None:
         """Parse branch name for use as deploy environment name."""
         if self.branch_name:
             if self.branch_name.startswith("ENV-"):
diff --git a/runway/core/components/_deployment.py b/runway/core/components/_deployment.py
index a073dff3e..1bbf79669 100644
--- a/runway/core/components/_deployment.py
+++ b/runway/core/components/_deployment.py
@@ -6,7 +6,7 @@
 import logging
 import multiprocessing
 import sys
-from typing import TYPE_CHECKING, Any, Optional, Union
+from typing import TYPE_CHECKING, Any, TypedDict
 
 from ..._logging import PrefixAdaptor
 from ...compat import cached_property
@@ -29,6 +29,15 @@
 LOGGER = logging.getLogger(__name__.replace("._", "."))
 
 
+class _AssumeRoleConfigTypeDef(TypedDict, total=False):
+    """Return type for :attr:`Deployment.assume_role_config`."""
+
+    duration_seconds: int
+    revert_on_exit: bool
+    role_arn: str
+    session_name: str
+
+
 class Deployment:
     """Runway deployment."""
 
@@ -36,8 +45,8 @@ def __init__(
         self,
         context: RunwayContext,
         definition: RunwayDeploymentDefinition,
-        future: Optional[RunwayFutureDefinitionModel] = None,
-        variables: Optional[RunwayVariablesDefinition] = None,
+        future: RunwayFutureDefinitionModel | None = None,
+        variables: RunwayVariablesDefinition | None = None,
     ) -> None:
         """Instantiate class.
 
@@ -57,22 +66,22 @@ def __init__(
         self.__merge_env_vars()
 
     @property
-    def assume_role_config(self) -> dict[str, Union[bool, int, str]]:
+    def assume_role_config(self) -> _AssumeRoleConfigTypeDef:
         """Parse the definition to get assume role arguments."""
         assume_role = self.definition.assume_role
         if not assume_role:
             self.logger.debug("assume_role not configured for deployment: %s", self.name)
             return {}
-        if isinstance(assume_role, str):  # type: ignore
+        if isinstance(assume_role, str):
             self.logger.debug("role found: %s", assume_role)
             assume_role = RunwayAssumeRoleDefinitionModel(arn=assume_role)
-        elif isinstance(assume_role, dict):  # type: ignore
-            assume_role = RunwayAssumeRoleDefinitionModel.parse_obj(assume_role)
+        elif isinstance(assume_role, dict):
+            assume_role = RunwayAssumeRoleDefinitionModel.model_validate(assume_role)
         if not assume_role.arn:
             self.logger.debug("assume_role not configured for deployment: %s", self.name)
             return {}
         return {
-            "duration_seconds": assume_role.duration,
+            "duration_seconds": int(assume_role.duration),
             "revert_on_exit": assume_role.post_deploy_env_revert,
             "role_arn": assume_role.arn,
             "session_name": assume_role.session_name,
@@ -178,7 +187,7 @@ def run(self, action: RunwayActionTypeDef, region: str) -> None:
                 variables=self._variables,
             )
 
-    def validate_account_credentials(self, context: Optional[RunwayContext] = None) -> None:
+    def validate_account_credentials(self, context: RunwayContext | None = None) -> None:
         """Exit if requested deployment account doesn't match credentials.
 
         Args:
@@ -238,7 +247,7 @@ def __async(self, action: RunwayActionTypeDef) -> None:
             max_workers=self.ctx.env.max_concurrent_regions,
             mp_context=multiprocessing.get_context("fork"),
         ) as executor:
-            futures = [executor.submit(self.run, *[action, region]) for region in self.regions]
+            futures = [executor.submit(self.run, action, region) for region in self.regions]
         for job in futures:
             job.result()  # raise exceptions / exit as needed
 
diff --git a/runway/core/components/_module.py b/runway/core/components/_module.py
index 73c2d8fe6..7199ef02c 100644
--- a/runway/core/components/_module.py
+++ b/runway/core/components/_module.py
@@ -7,7 +7,7 @@
 import logging
 import multiprocessing
 import sys
-from typing import TYPE_CHECKING, Any, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, cast
 
 import yaml
 
@@ -88,7 +88,7 @@ def child_modules(self) -> list[Module]:
         ]
 
     @cached_property
-    def environment_matches_defined(self) -> Optional[bool]:
+    def environment_matches_defined(self) -> bool | None:
         """Environment matches one of the defined environments.
 
         Will return None if there is nothing defined for the current environment.
@@ -259,7 +259,7 @@ def __async(self, action: RunwayActionTypeDef) -> None:
             max_workers=self.ctx.env.max_concurrent_modules,
             mp_context=multiprocessing.get_context("fork"),
         ) as executor:
-            futures = [executor.submit(child.run, *[action]) for child in self.child_modules]
+            futures = [executor.submit(child.run, action) for child in self.child_modules]
         for job in futures:
             job.result()  # raise exceptions / exit as needed
 
@@ -293,7 +293,7 @@ def run_list(
         modules: list[RunwayModuleDefinition],
         variables: RunwayVariablesDefinition,
         deployment: RunwayDeploymentDefinition = None,
-        future: Optional[RunwayFutureDefinitionModel] = None,
+        future: RunwayFutureDefinitionModel | None = None,
     ) -> None:
         """Run a list of modules.
 
@@ -327,9 +327,9 @@ def __getitem__(self, key: str) -> Any:
 
 def validate_environment(
     context: RunwayContext,
-    env_def: Optional[Union[bool, dict[str, Any], int, str, list[str]]],
-    logger: Union[PrefixAdaptor, RunwayLogger] = LOGGER,
-) -> Optional[bool]:
+    env_def: bool | dict[str, Any] | int | str | list[str] | None,
+    logger: PrefixAdaptor | RunwayLogger = LOGGER,
+) -> bool | None:
     """Check if an environment should be deployed to.
 
     Args:
@@ -349,14 +349,12 @@ def validate_environment(
         else:
             logger.verbose("environment not defined; module will determine deployment")
             env_def = None
-        return cast(Optional[bool], env_def)
+        return env_def
     if isinstance(env_def, dict):
         if context.env.name not in env_def:
             logger.info("skipped; environment not in definition")
             return False
-        return validate_environment(
-            context, cast(Any, env_def.get(context.env.name, False)), logger=logger
-        )
+        return validate_environment(context, env_def.get(context.env.name, False), logger=logger)
 
     account = aws.AccountDetails(context)
     accepted_values = [
diff --git a/runway/core/components/_module_type.py b/runway/core/components/_module_type.py
index b489f716f..24e1a7311 100644
--- a/runway/core/components/_module_type.py
+++ b/runway/core/components/_module_type.py
@@ -5,7 +5,7 @@
 import logging
 import os
 import sys
-from typing import TYPE_CHECKING, ClassVar, cast
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from typing_extensions import Literal
 
@@ -98,7 +98,7 @@ def __init__(
         self.type_str = type_str
         self.module_class = self._determine_module_class()
 
-    def _determine_module_class(self) -> type[RunwayModule]:
+    def _determine_module_class(self) -> type[RunwayModule[Any]]:
         """Determine type of module and return deployment module class.
 
         Returns:
@@ -131,7 +131,7 @@ def _determine_module_class(self) -> type[RunwayModule]:
             )
             sys.exit(1)
 
-        return cast(type["RunwayModule"], load_object_from_string(self.class_path))
+        return cast(type["RunwayModule[Any]"], load_object_from_string(self.class_path))
 
     def _set_class_path_based_on_extension(self) -> None:
         """Based on the directory suffix set the class_path."""
diff --git a/runway/core/providers/aws/_account.py b/runway/core/providers/aws/_account.py
index d5f8b4945..e75a8a3be 100644
--- a/runway/core/providers/aws/_account.py
+++ b/runway/core/providers/aws/_account.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING
 
 from ....compat import cached_property
 
@@ -15,7 +15,7 @@
 class AccountDetails:
     """AWS account details."""
 
-    def __init__(self, context: Union[CfnginContext, RunwayContext]) -> None:
+    def __init__(self, context: CfnginContext | RunwayContext) -> None:
         """Instantiate class.
 
         Args:
diff --git a/runway/core/providers/aws/_response.py b/runway/core/providers/aws/_response.py
index 7e00d7039..a06407e44 100644
--- a/runway/core/providers/aws/_response.py
+++ b/runway/core/providers/aws/_response.py
@@ -1,7 +1,7 @@
 """Base class for AWS responses."""
 
 from http import HTTPStatus
-from typing import Any, Dict  # noqa: UP035
+from typing import Any
 
 from pydantic import Field
 
@@ -45,7 +45,7 @@ class ResponseMetadata(BaseModel):
     host_id: str = Field(default="", alias="HostId")
     """Host ID data."""
 
-    https_headers: Dict[str, Any] = Field(default={}, alias="HTTPHeaders")  # noqa: UP006
+    https_headers: dict[str, Any] = Field(default={}, alias="HTTPHeaders")
     """A map of response header keys and their respective values."""
 
     http_status_code: int = Field(default=200, alias="HTTPStatusCode")
diff --git a/runway/core/providers/aws/s3/_bucket.py b/runway/core/providers/aws/s3/_bucket.py
index 4d22b18d8..0586fa280 100644
--- a/runway/core/providers/aws/s3/_bucket.py
+++ b/runway/core/providers/aws/s3/_bucket.py
@@ -73,14 +73,17 @@ def head(self) -> BaseResponse:
 
         """
         try:
-            return BaseResponse(**self.client.head_bucket(Bucket=self.name) or {})
+            return BaseResponse(
+                **self.client.head_bucket(Bucket=self.name)
+                or {}  # pyright: ignore[reportArgumentType]
+            )
         except ClientError as err:
             LOGGER.debug(
                 'received an error from AWS S3 when trying to head bucket "%s"',
                 self.name,
                 exc_info=True,
             )
-            return BaseResponse.parse_obj(err.response)
+            return BaseResponse.model_validate(err.response)
 
     @property
     def not_found(self) -> bool:
diff --git a/runway/core/providers/aws/s3/_helpers/action_architecture.py b/runway/core/providers/aws/s3/_helpers/action_architecture.py
index d778c384e..8f2cb1aaf 100644
--- a/runway/core/providers/aws/s3/_helpers/action_architecture.py
+++ b/runway/core/providers/aws/s3/_helpers/action_architecture.py
@@ -9,7 +9,7 @@
 
 import logging
 from queue import Queue
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
 
 from typing_extensions import Literal, TypedDict
 
@@ -124,13 +124,14 @@ def choose_sync_strategies(self) -> dict[str, BaseSync]:
         }
 
         # Determine what strategies to override if any.
-        responses: list[tuple[Any, BaseSync]] | None = self.botocore_session.emit(
-            "choosing-s3-sync-strategy", params=self.parameters
+        responses = cast(
+            "list[tuple[Any, BaseSync]] | None",
+            self.botocore_session.emit("choosing-s3-sync-strategy", params=self.parameters),
         )
         if responses is not None:
             for response in responses:
                 override_sync_strategy = response[1]
-                if override_sync_strategy is not None:
+                if override_sync_strategy:
                     sync_type = override_sync_strategy.sync_type
                     sync_type += "_sync_strategy"
                     sync_strategies[sync_type] = override_sync_strategy
diff --git a/runway/core/providers/aws/s3/_helpers/file_generator.py b/runway/core/providers/aws/s3/_helpers/file_generator.py
index 8c37e2438..8cfe149cb 100644
--- a/runway/core/providers/aws/s3/_helpers/file_generator.py
+++ b/runway/core/providers/aws/s3/_helpers/file_generator.py
@@ -16,8 +16,6 @@
 from typing import (
     TYPE_CHECKING,
     Any,
-    Optional,
-    Union,
     cast,
 )
 
@@ -197,7 +195,7 @@ def call(self, files: FormatPathResult) -> Generator[FileStats, None, None]:
             }
             if files["src"]["type"] == "s3":
                 file_stat_kwargs["response_data"] = cast(
-                    Optional[Union["HeadObjectOutputTypeDef", "ObjectTypeDef"]],
+                    "HeadObjectOutputTypeDef | ObjectTypeDef | None",
                     extra_information,
                 )
             yield FileStats(**file_stat_kwargs)
diff --git a/runway/core/providers/aws/s3/_helpers/file_info_builder.py b/runway/core/providers/aws/s3/_helpers/file_info_builder.py
index 9a81f88e3..279fe16b4 100644
--- a/runway/core/providers/aws/s3/_helpers/file_info_builder.py
+++ b/runway/core/providers/aws/s3/_helpers/file_info_builder.py
@@ -63,7 +63,7 @@ def _inject_info(self, file_base: FileStats) -> FileInfo:
         """
         delete_enabled = self._parameters.delete if self._parameters else False
         return FileInfo(
-            **file_base.dict(),
+            **file_base.dict(),  # pyright: ignore[reportArgumentType]
             **(
                 {"client": self._source_client, "source_client": self._client}
                 if file_base.operation_name == "delete" and delete_enabled
diff --git a/runway/core/providers/aws/s3/_helpers/filters.py b/runway/core/providers/aws/s3/_helpers/filters.py
index dbe57d4e2..f26a60d66 100644
--- a/runway/core/providers/aws/s3/_helpers/filters.py
+++ b/runway/core/providers/aws/s3/_helpers/filters.py
@@ -14,7 +14,6 @@
     TYPE_CHECKING,
     ClassVar,
     NamedTuple,
-    cast,
 )
 
 from typing_extensions import Literal
@@ -161,7 +160,7 @@ def parse_params(cls, parameters: ParametersDataModel) -> Filter:
         filter_patterns: set[FilterPattern] = set()
         for filter_type in cls.FILTER_TYPES:
             for pat in parameters[filter_type]:
-                filter_patterns.add(FilterPattern(type=cast(_FilterType, filter_type), pattern=pat))
+                filter_patterns.add(FilterPattern(type=filter_type, pattern=pat))
         return Filter(
             filter_patterns,
             cls.parse_rootdir(parameters.src),
diff --git a/runway/core/providers/aws/s3/_helpers/parameters.py b/runway/core/providers/aws/s3/_helpers/parameters.py
index e98722567..637fe139f 100644
--- a/runway/core/providers/aws/s3/_helpers/parameters.py
+++ b/runway/core/providers/aws/s3/_helpers/parameters.py
@@ -1,17 +1,19 @@
 """Parameters."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
 from pathlib import Path
-from typing import Any, List, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 
-from pydantic import validator
+from pydantic import field_validator, model_validator
 from typing_extensions import Literal
 
 from ......utils import BaseModel
 from .utils import find_bucket_key
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
 PathsType = Literal["local", "locallocal", "locals3", "s3", "s3local", "s3s3"]
 
 
@@ -50,46 +52,39 @@ class ParametersDataModel(BaseModel):
     dest: str
     src: str
     # these need to be set after dest & src so their validators can access the value if needed
-    content_type: Optional[str] = None
+    content_type: str | None = None
     delete: bool = False
     dir_op: bool = False
     dryrun: bool = False
     exact_timestamps: bool = False
-    exclude: List[str] = []
-    expected_size: Optional[int] = None
+    exclude: list[str] = []
+    expected_size: int | None = None
     follow_symlinks: bool = False
     force_glacier_transfer: bool = False
     guess_mime_type: bool = True
     ignore_glacier_warnings: bool = False
-    include: List[str] = []
+    include: list[str] = []
     is_move: bool = False
     is_stream: bool = False
     no_progress: bool = False
     only_show_errors: bool = False
-    page_size: Optional[int] = None
+    page_size: int | None = None
     paths_type: PathsType = "local"  # will be overwritten
     quiet: bool = False
     size_only: bool = False
-    sse_c: Optional[str] = None
-    sse_c_key: Optional[str] = None
-    storage_class: Optional[str] = None
+    sse_c: str | None = None
+    sse_c_key: str | None = None
+    storage_class: str | None = None
 
-    @validator("paths_type", always=True, pre=True)  # type: ignore
-    @classmethod
-    def _determine_paths_type(
-        cls,
-        v: str | None,  # noqa: ARG003
-        values: dict[str, Any],
-    ) -> PathsType:
+    @model_validator(mode="after")
+    def _determine_paths_type(self: Self) -> Self:
         """Determine paths type for the given src and dest."""
-        # these have already been validated so it's "safe" to cast them
-        dest = cast(str, values.get("dest", ""))
-        src = cast(str, values.get("src", ""))
-        src_type = "s3" if src.startswith("s3://") else "local"
-        dest_type = "s3" if dest.startswith("s3://") else "local"
-        return cast(PathsType, f"{src_type}{dest_type}")
-
-    @validator("dest", "src", pre=True)  # type: ignore
+        src_type = "s3" if self.src.startswith("s3://") else "local"
+        dest_type = "s3" if self.dest.startswith("s3://") else "local"
+        self.paths_type = cast(PathsType, f"{src_type}{dest_type}")
+        return self
+
+    @field_validator("dest", "src", mode="before")
     @classmethod
     def _normalize_s3_trailing_slash(cls, v: str) -> str:
         """Add a trailing "/" if the root of an S3 bucket was provided."""
@@ -112,7 +107,7 @@ def __init__(self, action: str, parameters: dict[str, Any] | ParametersDataModel
 
         """
         self.action = action
-        self.data = ParametersDataModel.parse_obj(parameters)
+        self.data = ParametersDataModel.model_validate(parameters)
         if self.action in ["sync", "mb", "rb"]:
             self.data.dir_op = True
         if self.action == "mv":
diff --git a/runway/core/providers/aws/s3/_helpers/results.py b/runway/core/providers/aws/s3/_helpers/results.py
index 042dc659f..7f667ccc5 100644
--- a/runway/core/providers/aws/s3/_helpers/results.py
+++ b/runway/core/providers/aws/s3/_helpers/results.py
@@ -21,7 +21,7 @@
     ClassVar,
     NamedTuple,
     TextIO,
-    Union,
+    TypedDict,
     cast,
 )
 
@@ -40,7 +40,7 @@
     from types import TracebackType
 
     from s3transfer.futures import TransferFuture
-    from typing_extensions import Literal, Self
+    from typing_extensions import Literal, Self, TypeAlias
 
     from ......_logging import RunwayLogger
     from ......type_defs import AnyPath
@@ -142,17 +142,23 @@ class QueuedResult(NamedTuple):
     SuccessResult,
     QueuedResult,
 )
-AnyResult = Union[
-    CommandResult,
-    CtrlCResult,
-    DryRunResult,
-    ErrorResult,
-    FailureResult,
-    FinalTotalSubmissionsResult,
-    ProgressResult,
-    QueuedResult,
-    SuccessResult,
-]
+AnyResultType: TypeAlias = (
+    "CommandResult | CtrlCResult | DryRunResult | ErrorResult | FailureResult | FinalTotalSubmissionsResult | ProgressResult | QueuedResult | SuccessResult"  # noqa: E501
+)
+
+
+class _ResultHandlerMappingTypedDict(TypedDict, total=False):
+
+    CommandResult: Callable[..., None]
+    CtrlCResult: Callable[..., None]
+    DryRunResult: Callable[..., None]
+    ErrorResult: Callable[..., None]
+    FailureResult: Callable[..., None]
+    FinalTotalSubmissionsResult: Callable[..., None]
+    PrintTask: Callable[..., None]
+    ProgressResult: Callable[..., None]
+    QueuedResult: Callable[..., None]
+    SuccessResult: Callable[..., None]
 
 
 class ShutdownThreadRequest:
@@ -162,7 +168,7 @@ class ShutdownThreadRequest:
 class BaseResultSubscriber(OnDoneFilteredSubscriber):
     """Base result subscriber."""
 
-    TRANSFER_TYPE: ClassVar[str | None] = None
+    TRANSFER_TYPE: ClassVar = None
 
     def __init__(self, result_queue: queue.Queue[Any], transfer_type: str | None = None) -> None:
         """Send result notifications during transfer process.
@@ -333,27 +339,27 @@ def __init__(self) -> None:
         self._ongoing_progress = defaultdict(int)
         self._ongoing_total_sizes: dict[str, int] = {}
 
-        self._result_handler_map = {
-            QueuedResult: self._record_queued_result,
-            ProgressResult: self._record_progress_result,
-            SuccessResult: self._record_success_result,
-            FailureResult: self._record_failure_result,
-            PrintTask: self._record_warning_result,
-            ErrorResult: self._record_error_result,
-            CtrlCResult: self._record_error_result,
-            FinalTotalSubmissionsResult: self._record_final_expected_files,
+        self._result_handler_map: _ResultHandlerMappingTypedDict = {
+            "CtrlCResult": self._record_error_result,
+            "ErrorResult": self._record_error_result,
+            "FailureResult": self._record_failure_result,
+            "FinalTotalSubmissionsResult": self._record_final_expected_files,
+            "PrintTask": self._record_warning_result,
+            "ProgressResult": self._record_progress_result,
+            "QueuedResult": self._record_queued_result,
+            "SuccessResult": self._record_success_result,
         }
 
     def expected_totals_are_final(self) -> bool:
         """Assess if expected totals are final."""
         return self.final_expected_files_transferred == self.expected_files_transferred
 
-    def __call__(self, result: Any) -> None:
+    def __call__(self, result: AnyResultType | PrintTask) -> None:
         """Record the result of an individual Result object."""
-        self._result_handler_map.get(type(result), self._record_noop)(result=result)
+        self._result_handler_map.get(type(result).__name__, self._record_noop)(result=result)
 
     @staticmethod
-    def _get_ongoing_dict_key(result: AnyResult | object) -> str:
+    def _get_ongoing_dict_key(result: AnyResultType | object) -> str:
         if not isinstance(result, AllResultTypes):
             raise TypeError(
                 "Any result using _get_ongoing_dict_key must be one of "
@@ -366,7 +372,7 @@ def _get_ongoing_dict_key(result: AnyResult | object) -> str:
                 key_parts.append(ensure_string(result_property))  # noqa: PERF401
         return ":".join(key_parts)
 
-    def _pop_result_from_ongoing_dicts(self, result: AnyResult) -> tuple[int, int | None]:
+    def _pop_result_from_ongoing_dicts(self, result: AnyResultType) -> tuple[int, int | None]:
         ongoing_key = self._get_ongoing_dict_key(result)
         total_progress = self._ongoing_progress.pop(ongoing_key, 0)
         total_file_size = self._ongoing_total_sizes.pop(ongoing_key, None)
@@ -413,7 +419,9 @@ def _update_ongoing_transfer_size_if_unknown(self, result: ProgressResult) -> No
             # If the total size is no longer None that means we just learned
             # of the size so let's update the appropriate places with this
             # knowledge
-            if result.total_transfer_size is not None:
+            if (
+                result.total_transfer_size is not None
+            ):  # pyright: ignore[reportUnnecessaryComparison]
                 self._ongoing_total_sizes[ongoing_key] = total_transfer_size
                 # Figure out how many bytes have been unaccounted for as
                 # the recorder has been keeping track of how many bytes
@@ -427,11 +435,11 @@ def _update_ongoing_transfer_size_if_unknown(self, result: ProgressResult) -> No
             else:
                 self.expected_bytes_transferred += result.bytes_transferred
 
-    def _record_success_result(self, result: AnyResult, **_: Any) -> None:
+    def _record_success_result(self, result: AnyResultType, **_: Any) -> None:
         self._pop_result_from_ongoing_dicts(result)
         self.files_transferred += 1
 
-    def _record_failure_result(self, result: AnyResult, **_: Any) -> None:
+    def _record_failure_result(self, result: AnyResultType, **_: Any) -> None:
         """On failure, account for the failure in count for bytes transferred."""
         total_progress, total_file_size = self._pop_result_from_ongoing_dicts(result)
         if total_file_size is not None:
@@ -499,20 +507,20 @@ def __init__(
         if self._error_file is None:
             self._error_file = sys.stderr
         self._progress_length = 0
-        self._result_handler_map = {
-            ProgressResult: self._print_progress,
-            SuccessResult: self._print_success,
-            FailureResult: self._print_failure,
-            PrintTask: self._print_warning,
-            ErrorResult: self._print_error,
-            CtrlCResult: self._print_ctrl_c,
-            DryRunResult: self._print_dry_run,
-            FinalTotalSubmissionsResult: self._clear_progress_if_no_more_expected_transfers,
+        self._result_handler_map: _ResultHandlerMappingTypedDict = {
+            "CtrlCResult": self._print_ctrl_c,
+            "DryRunResult": self._print_dry_run,
+            "ErrorResult": self._print_error,
+            "FailureResult": self._print_failure,
+            "FinalTotalSubmissionsResult": self._clear_progress_if_no_more_expected_transfers,
+            "PrintTask": self._print_warning,
+            "ProgressResult": self._print_progress,
+            "SuccessResult": self._print_success,
         }
 
-    def __call__(self, result: Any) -> None:
+    def __call__(self, result: AnyResultType | PrintTask) -> None:
         """Print the progress of the ongoing transfer based on a result."""
-        self._result_handler_map.get(type(result), self._print_noop)(result=result)
+        self._result_handler_map.get(type(result).__name__, self._print_noop)(result=result)
 
     def _print_noop(self, **_: Any) -> None:
         """If result does not have a handler, then do nothing with it."""
@@ -552,7 +560,7 @@ def _print_error(self, result: ErrorResult, **_: Any) -> None:
     def _print_ctrl_c(self, result: CtrlCResult, **_: Any) -> None:  # noqa: ARG002
         LOGGER.warning(self.CTRL_C_MSG)
 
-    def _get_transfer_location(self, result: AnyResult) -> str:
+    def _get_transfer_location(self, result: AnyResultType) -> str:
         if result.dest is None:
             return self.SRC_TRANSFER_LOCATION_FORMAT.format(src=result.src)
         return self.SRC_DEST_TRANSFER_LOCATION_FORMAT.format(src=result.src, dest=result.dest)
@@ -706,7 +714,7 @@ def run(self) -> None:
             except queue.Empty:  # cov: ignore
                 pass
 
-    def _process_result(self, result: AnyResult) -> None:
+    def _process_result(self, result: AnyResultType) -> None:
         for result_handler in self._result_handlers:
             try:
                 result_handler(result)
diff --git a/runway/core/providers/aws/s3/_helpers/s3handler.py b/runway/core/providers/aws/s3/_helpers/s3handler.py
index a580b1f3d..40b359c36 100644
--- a/runway/core/providers/aws/s3/_helpers/s3handler.py
+++ b/runway/core/providers/aws/s3/_helpers/s3handler.py
@@ -36,7 +36,6 @@
     ResultProcessor,
     ResultRecorder,
     SuccessResult,
-    Union,
     UploadResultSubscriber,
     UploadStreamResultSubscriber,
 )
@@ -147,12 +146,7 @@ def _add_result_printer(
         self,
         result_recorder: ResultRecorder,
         result_processor_handlers: list[
-            Union[
-                NoProgressResultPrinter,
-                OnlyShowErrorsResultPrinter,
-                ResultPrinter,
-                ResultRecorder,
-            ]
+            NoProgressResultPrinter | OnlyShowErrorsResultPrinter | ResultPrinter | ResultRecorder
         ],
     ) -> None:
         if self._config_params.quiet:
@@ -414,10 +408,10 @@ def _format_s3_path(self, path: AnyPath | None) -> str | None:
 class UploadRequestSubmitter(BaseTransferRequestSubmitter):
     """Upload request submitter."""
 
-    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any]] = (
+    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any] | None] = (
         RequestParamsMapper.map_put_object_params
     )
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[UploadResultSubscriber]] = UploadResultSubscriber
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = UploadResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
@@ -490,10 +484,10 @@ def _format_src_dest(self, fileinfo: FileInfo) -> tuple[str | None, str | None]:
 class DownloadRequestSubmitter(BaseTransferRequestSubmitter):
     """Download request submitter."""
 
-    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any]] = (
+    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any] | None] = (
         RequestParamsMapper.map_get_object_params
     )
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[DownloadResultSubscriber]] = DownloadResultSubscriber
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = DownloadResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
@@ -556,10 +550,10 @@ def _format_src_dest(self, fileinfo: FileInfo) -> tuple[str | None, str | None]:
 class CopyRequestSubmitter(BaseTransferRequestSubmitter):
     """Copy request submitter."""
 
-    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any]] = (
+    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any] | None] = (
         RequestParamsMapper.map_copy_object_params
     )
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[CopyResultSubscriber]] = CopyResultSubscriber
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = CopyResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
@@ -620,9 +614,7 @@ def _format_src_dest(self, fileinfo: FileInfo) -> tuple[str | None, str | None]:
 class UploadStreamRequestSubmitter(UploadRequestSubmitter):
     """Upload stream request submitter."""
 
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[UploadStreamResultSubscriber]] = (
-        UploadStreamResultSubscriber
-    )
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = UploadStreamResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
@@ -661,9 +653,7 @@ def _format_local_path(self, path: AnyPath | None) -> str:  # noqa: ARG002
 class DownloadStreamRequestSubmitter(DownloadRequestSubmitter):
     """Download stream result subscriber."""
 
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[DownloadStreamResultSubscriber]] = (
-        DownloadStreamResultSubscriber
-    )
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = DownloadStreamResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
@@ -697,10 +687,10 @@ def _format_local_path(self, path: AnyPath | None) -> str:  # noqa: ARG002
 class DeleteRequestSubmitter(BaseTransferRequestSubmitter):
     """Delete request submitter."""
 
-    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any]] = (
+    REQUEST_MAPPER_METHOD: ClassVar[Callable[[dict[Any, Any], dict[Any, Any]], Any] | None] = (
         RequestParamsMapper.map_delete_object_params
     )
-    RESULT_SUBSCRIBER_CLASS: ClassVar[type[DeleteResultSubscriber]] = DeleteResultSubscriber
+    RESULT_SUBSCRIBER_CLASS: ClassVar[type[BaseSubscriber] | None] = DeleteResultSubscriber
 
     def can_submit(self, fileinfo: FileInfo) -> bool:
         """Check whether it can submit a particular FileInfo.
diff --git a/runway/core/providers/aws/s3/_helpers/sync_strategy/base.py b/runway/core/providers/aws/s3/_helpers/sync_strategy/base.py
index 3fe1ecf41..580091e1c 100644
--- a/runway/core/providers/aws/s3/_helpers/sync_strategy/base.py
+++ b/runway/core/providers/aws/s3/_helpers/sync_strategy/base.py
@@ -36,7 +36,7 @@ class BaseSync:
 
     """
 
-    NAME: ClassVar[str | None] = None
+    NAME: ClassVar = None
 
     sync_type: ValidSyncType
 
diff --git a/runway/core/providers/aws/s3/_helpers/utils.py b/runway/core/providers/aws/s3/_helpers/utils.py
index 2c22fb4e2..cac0c94f3 100644
--- a/runway/core/providers/aws/s3/_helpers/utils.py
+++ b/runway/core/providers/aws/s3/_helpers/utils.py
@@ -37,7 +37,7 @@
     from queue import Queue
 
     from mypy_boto3_s3.client import S3Client
-    from mypy_boto3_s3.type_defs import ObjectTypeDef
+    from mypy_boto3_s3.type_defs import DeleteObjectRequestRequestTypeDef, ObjectTypeDef
     from s3transfer.futures import TransferFuture
     from s3transfer.utils import CallArgs
 
@@ -210,7 +210,7 @@ def _get_key(call_args: CallArgs) -> str:
     def _delete_source(self, future: TransferFuture) -> None:
         """Delete source."""
         call_args = future.meta.call_args
-        delete_object_kwargs = {
+        delete_object_kwargs: DeleteObjectRequestRequestTypeDef = {
             "Bucket": self._get_bucket(call_args),
             "Key": self._get_key(call_args),
         }
@@ -914,14 +914,13 @@ def uni_print(statement: str, out_file: TextIO | None = None) -> None:
     Ensures that the proper encoding is used if the statement is not a string type.
 
     """
-    if out_file is None:
-        out_file = sys.stdout
+    out: TextIO | Any = sys.stdout if out_file is None else out_file
     try:
-        out_file.write(statement)
+        out.write(statement)
     except UnicodeEncodeError:
-        new_encoding = getattr(out_file, "encoding", "ascii")
+        new_encoding = getattr(out, "encoding", "ascii")
         if not new_encoding:
             new_encoding = "ascii"
         new_statement = statement.encode(new_encoding, "replace").decode(new_encoding)
-        out_file.write(new_statement)
-    out_file.flush()
+        out.write(new_statement)
+    out.flush()
diff --git a/runway/core/providers/aws/s3/_sync_handler.py b/runway/core/providers/aws/s3/_sync_handler.py
index c0aa23254..2f4c509c4 100644
--- a/runway/core/providers/aws/s3/_sync_handler.py
+++ b/runway/core/providers/aws/s3/_sync_handler.py
@@ -80,15 +80,17 @@ def client(self) -> S3Client:
     def transfer_config(self) -> TransferConfigDict:
         """Get runtime transfer config."""
         return RuntimeConfig.build_config(
-            **self._botocore_session.get_scoped_config().get("s3", {})
+            **self._botocore_session.get_scoped_config().get(  # pyright: ignore[reportUnknownArgumentType]
+                "s3", {}
+            )
         )
 
     def run(self) -> None:
         """Run sync."""
-        register_sync_strategies(self._botocore_session)  # type: ignore
+        register_sync_strategies(self._botocore_session)
         ActionArchitecture(
             session=self._session,
-            botocore_session=self._botocore_session,  # type: ignore
+            botocore_session=self._botocore_session,
             action="sync",
             parameters=self.parameters.data,
             runtime_config=self.transfer_config,
diff --git a/runway/dependency_managers/_pip.py b/runway/dependency_managers/_pip.py
index 1480f5a1f..e7150f457 100644
--- a/runway/dependency_managers/_pip.py
+++ b/runway/dependency_managers/_pip.py
@@ -6,7 +6,7 @@
 import re
 import subprocess
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Final, Optional, cast
+from typing import TYPE_CHECKING, Any, ClassVar, cast
 
 from ..compat import cached_property, shlex_join
 from ..exceptions import RunwayError
@@ -17,7 +17,6 @@
     from collections.abc import Iterable
 
     from _typeshed import StrPath
-    from typing_extensions import Literal
 
     from .._logging import RunwayLogger
 
@@ -39,10 +38,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
 class Pip(DependencyManager):
     """pip CLI interface."""
 
-    CONFIG_FILES: Final[tuple[Literal["requirements.txt"]]] = ("requirements.txt",)
+    CONFIG_FILES: ClassVar[tuple[str, ...]] = ("requirements.txt",)
     """Configuration files used by pip."""
 
-    EXECUTABLE: Final[Literal["pip"]] = "pip"
+    EXECUTABLE: ClassVar[str] = "pip"
     """CLI executable."""
 
     @cached_property
@@ -82,7 +81,7 @@ def dir_is_project(cls, directory: StrPath, **kwargs: Any) -> bool:
     def generate_install_command(
         cls,
         *,
-        cache_dir: Optional[StrPath] = None,
+        cache_dir: StrPath | None = None,
         no_cache_dir: bool = False,
         no_deps: bool = False,
         requirements: StrPath,
@@ -115,8 +114,8 @@ def generate_install_command(
     def install(
         self,
         *,
-        cache_dir: Optional[StrPath] = None,
-        extend_args: Optional[list[str]] = None,
+        cache_dir: StrPath | None = None,
+        extend_args: list[str] | None = None,
         no_cache_dir: bool = False,
         no_deps: bool = False,
         requirements: StrPath,
diff --git a/runway/dependency_managers/_pipenv.py b/runway/dependency_managers/_pipenv.py
index e6921c04c..17d03f439 100644
--- a/runway/dependency_managers/_pipenv.py
+++ b/runway/dependency_managers/_pipenv.py
@@ -7,7 +7,7 @@
 import re
 import subprocess
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ..compat import cached_property
 from ..exceptions import RunwayError
@@ -16,7 +16,6 @@
 
 if TYPE_CHECKING:
     from _typeshed import StrPath
-    from typing_extensions import Literal
 
 LOGGER = logging.getLogger(__name__)
 
@@ -49,13 +48,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
 class Pipenv(DependencyManager):
     """Pipenv dependency manager."""
 
-    CONFIG_FILES: Final[tuple[Literal["Pipfile"], Literal["Pipfile.lock"]]] = (
+    CONFIG_FILES: ClassVar[tuple[str, ...]] = (
         "Pipfile",
         "Pipfile.lock",
     )
     """Configuration files used by pipenv."""
 
-    EXECUTABLE: Final[Literal["pipenv"]] = "pipenv"
+    EXECUTABLE: ClassVar[str] = "pipenv"
     """CLI executable."""
 
     @cached_property
diff --git a/runway/dependency_managers/_poetry.py b/runway/dependency_managers/_poetry.py
index 27d3744c1..d2b982716 100644
--- a/runway/dependency_managers/_poetry.py
+++ b/runway/dependency_managers/_poetry.py
@@ -6,7 +6,7 @@
 import re
 import subprocess
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Final, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 
 import tomli
 
@@ -17,7 +17,6 @@
 
 if TYPE_CHECKING:
     from _typeshed import StrPath
-    from typing_extensions import Literal
 
 LOGGER = logging.getLogger(__name__)
 
@@ -54,13 +53,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
 class Poetry(DependencyManager):
     """Poetry dependency manager."""
 
-    CONFIG_FILES: Final[tuple[Literal["poetry.lock"], Literal["pyproject.toml"]]] = (
+    CONFIG_FILES: ClassVar[tuple[str, ...]] = (
         "poetry.lock",
         "pyproject.toml",
     )
     """Configuration files used by poetry."""
 
-    EXECUTABLE: Final[Literal["poetry"]] = "poetry"
+    EXECUTABLE: ClassVar[str] = "poetry"
     """CLI executable."""
 
     @cached_property
@@ -88,9 +87,7 @@ def dir_is_project(cls, directory: StrPath, **__kwargs: Any) -> bool:
 
         # check for PEP-517 definition
         pyproject = tomli.loads(pyproject_path.read_text())
-        build_system_requires: Optional[list[str]] = pyproject.get("build-system", {}).get(
-            "requires"
-        )
+        build_system_requires: list[str] | None = pyproject.get("build-system", {}).get("requires")
 
         if build_system_requires:
             for req in build_system_requires:
@@ -103,7 +100,7 @@ def export(
         self,
         *,
         dev: bool = False,
-        extras: Optional[list[str]] = None,
+        extras: list[str] | None = None,
         output: StrPath,
         output_format: str = "requirements.txt",
         with_credentials: bool = True,
diff --git a/runway/dependency_managers/base_classes.py b/runway/dependency_managers/base_classes.py
index 41509f1d7..40a7151d9 100644
--- a/runway/dependency_managers/base_classes.py
+++ b/runway/dependency_managers/base_classes.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, ClassVar, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ..compat import cached_property
 from ..mixins import CliInterfaceMixin
@@ -27,7 +27,7 @@ class DependencyManager(CliInterfaceMixin):
     CONFIG_FILES: ClassVar[tuple[str, ...]]
     """Configuration files used by the dependency manager."""
 
-    def __init__(self, context: Union[CfnginContext, RunwayContext], cwd: StrPath) -> None:
+    def __init__(self, context: CfnginContext | RunwayContext, cwd: StrPath) -> None:
         """Instantiate class.
 
         Args:
diff --git a/runway/env_mgr/__init__.py b/runway/env_mgr/__init__.py
index 6e110c575..e06635f65 100644
--- a/runway/env_mgr/__init__.py
+++ b/runway/env_mgr/__init__.py
@@ -8,7 +8,7 @@
 import shutil
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union, cast
+from typing import TYPE_CHECKING, cast
 
 from ..compat import cached_property
 from ..mixins import DelCachedPropMixin
@@ -68,11 +68,11 @@ class EnvManager(DelCachedPropMixin):
 
     _bin_name: str
 
-    current_version: Optional[str]
+    current_version: str | None
     env_dir_name: str
     path: Path
 
-    def __init__(self, bin_name: str, dir_name: str, path: Optional[Path] = None) -> None:
+    def __init__(self, bin_name: str, dir_name: str, path: Path | None = None) -> None:
         """Initialize class.
 
         Args:
@@ -126,7 +126,7 @@ def versions_dir(self) -> Path:
         return self.env_dir / "versions"
 
     @cached_property
-    def version_file(self) -> Optional[Path]:
+    def version_file(self) -> Path | None:
         """Find and return a "<bin version file>" file if one is present.
 
         Returns:
@@ -135,7 +135,7 @@ def version_file(self) -> Optional[Path]:
         """
         raise NotImplementedError
 
-    def install(self, version_requested: Optional[str] = None) -> str:
+    def install(self, version_requested: str | None = None) -> str:
         """Ensure <bin> is installed."""
         raise NotImplementedError
 
@@ -143,7 +143,7 @@ def list_installed(self) -> Generator[Path, None, None]:
         """List installed versions of <bin>."""
         raise NotImplementedError
 
-    def uninstall(self, version: Union[str, Version]) -> bool:
+    def uninstall(self, version: str | Version) -> bool:
         """Uninstall a version of the managed binary.
 
         Args:
diff --git a/runway/env_mgr/kbenv.py b/runway/env_mgr/kbenv.py
index 4c9808375..6ad1b28f1 100644
--- a/runway/env_mgr/kbenv.py
+++ b/runway/env_mgr/kbenv.py
@@ -11,7 +11,7 @@
 import shutil
 import sys
 import tempfile
-from typing import TYPE_CHECKING, Final, Optional, cast
+from typing import TYPE_CHECKING, Final, cast
 from urllib.error import URLError
 from urllib.request import urlretrieve
 
@@ -106,8 +106,8 @@ def verify_kb_release(kb_url: str, download_dir: str, filename: str) -> None:
 def download_kb_release(
     version: str,
     versions_dir: Path,
-    kb_platform: Optional[str] = None,
-    arch: Optional[str] = None,
+    kb_platform: str | None = None,
+    arch: str | None = None,
 ) -> None:
     """Download kubectl and return path to it."""
     version_dir = versions_dir / version
@@ -159,7 +159,7 @@ class KBEnvManager(EnvManager):
 
     VERSION_REGEX: Final[str] = r"^(v)?(?P<version>[0-9]+\.[0-9]+\.[0-9]+\S*)"
 
-    def __init__(self, path: Optional[Path] = None, *, overlay_path: Optional[Path] = None) -> None:
+    def __init__(self, path: Path | None = None, *, overlay_path: Path | None = None) -> None:
         """Initialize class.
 
         Args:
@@ -171,7 +171,7 @@ def __init__(self, path: Optional[Path] = None, *, overlay_path: Optional[Path]
         self.overlay_path = overlay_path
 
     @cached_property
-    def version(self) -> Optional[Version]:
+    def version(self) -> Version | None:
         """Terraform version."""
         if not self.current_version:
             self.current_version = self.get_version_from_file()
@@ -180,7 +180,7 @@ def version(self) -> Optional[Version]:
         return self.parse_version_string(self.current_version)
 
     @cached_property
-    def version_file(self) -> Optional[Path]:
+    def version_file(self) -> Path | None:
         """Find and return a ".kubectl-version" file if one is present.
 
         Returns:
@@ -197,7 +197,7 @@ def version_file(self) -> Optional[Path]:
                 return tmp_path
         return None
 
-    def get_version_from_file(self, file_path: Optional[Path] = None) -> Optional[str]:
+    def get_version_from_file(self, file_path: Path | None = None) -> str | None:
         """Get kubectl version from a file.
 
         Args:
@@ -212,7 +212,7 @@ def get_version_from_file(self, file_path: Optional[Path] = None) -> Optional[st
         LOGGER.debug("file path not provided and version file could not be found")
         return None
 
-    def install(self, version_requested: Optional[str] = None) -> str:
+    def install(self, version_requested: str | None = None) -> str:
         """Ensure kubectl is available."""
         if not version_requested:
             if self.version:
diff --git a/runway/env_mgr/tfenv.py b/runway/env_mgr/tfenv.py
index d78513b74..f5ad4efa8 100644
--- a/runway/env_mgr/tfenv.py
+++ b/runway/env_mgr/tfenv.py
@@ -138,7 +138,7 @@ def load_terraform_module(parser: ModuleType, path: Path) -> dict[str, Any]:
     LOGGER.debug("using %s parser to load module: %s", parser.__name__.upper(), path)
     for tf_file in path.glob("*.tf"):
         try:
-            tf_config = parser.loads(tf_file.read_text())  # type: ignore
+            tf_config = parser.loads(tf_file.read_text())
             result = merge_dicts(result, cast("dict[str, Any]", tf_config))
         except Exception as exc:  # noqa: BLE001
             raise HclParserError(exc, tf_file, parser) from None
@@ -200,7 +200,7 @@ def _flatten_lists(data: dict[str, Any] | list[Any] | Any) -> dict[str, Any] | A
             """
             if not isinstance(data, dict):
                 return data
-            copy_data = cast("dict[str, Any]", data.copy())
+            copy_data = data.copy()
             for attr, val in copy_data.items():
                 if isinstance(val, list):
                     if len(cast("list[Any]", val)) == 1:
diff --git a/runway/lookups/handlers/base.py b/runway/lookups/handlers/base.py
index 5e8f72339..eab955b5d 100644
--- a/runway/lookups/handlers/base.py
+++ b/runway/lookups/handlers/base.py
@@ -5,7 +5,7 @@
 import json
 import logging
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
 
 import yaml
 from troposphere import BaseAWSObject
@@ -29,6 +29,21 @@ def str2bool(v: str) -> bool:
     return v.lower() in ("yes", "true", "t", "1", "on", "y")
 
 
+class ParsedArgsTypeDef(TypedDict, total=False):
+    """Partial type definition for the args returned by :meth:`LookupHandler.parse`.
+
+    This class can be subclassed to model all expected arguments if needed.
+
+    """
+
+    default: str
+    get: str
+    indent: str
+    load: Literal["json", "troposphere", "yaml"]
+    region: str
+    transform: TransformToTypeLiteral
+
+
 class LookupHandler:
     """Base class for lookup handlers."""
 
@@ -120,7 +135,7 @@ def handle(
         raise NotImplementedError
 
     @classmethod
-    def parse(cls, value: str) -> tuple[str, dict[str, str]]:
+    def parse(cls, value: str) -> tuple[str, ParsedArgsTypeDef]:
         """Parse the value passed to a lookup in a standardized way.
 
         Args:
@@ -135,9 +150,9 @@ def parse(cls, value: str) -> tuple[str, dict[str, str]]:
         colon_split = raw_value.split("::", 1)
 
         query = colon_split.pop(0)
-        args: dict[str, str] = cls._parse_args(colon_split[0]) if colon_split else {}
+        args = cls._parse_args(colon_split[0]) if colon_split else {}
 
-        return query, args
+        return query, cast(ParsedArgsTypeDef, args)
 
     @classmethod
     def _parse_args(cls, args: str) -> dict[str, str]:
@@ -270,7 +285,7 @@ def transform(
         if not to_type:
             return value
 
-        return mapping[to_type](value, **kwargs)  # type: ignore
+        return mapping[to_type](value, **kwargs)
 
     @classmethod
     def _transform_to_bool(cls, value: Any, **_: Any) -> bool:
diff --git a/runway/lookups/handlers/cfn.py b/runway/lookups/handlers/cfn.py
index 4f038925c..6747ae5d1 100644
--- a/runway/lookups/handlers/cfn.py
+++ b/runway/lookups/handlers/cfn.py
@@ -11,7 +11,7 @@
 
 import json
 import logging
-from typing import TYPE_CHECKING, Any, Final, NamedTuple, cast
+from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast
 
 from botocore.exceptions import ClientError
 
@@ -21,10 +21,10 @@
 
 if TYPE_CHECKING:
     from mypy_boto3_cloudformation.client import CloudFormationClient
-    from typing_extensions import Literal
 
     from ...cfngin.providers.aws.default import Provider
     from ...context import CfnginContext, RunwayContext
+    from .base import ParsedArgsTypeDef
 
 LOGGER = logging.getLogger(__name__)
 
@@ -39,11 +39,11 @@ class OutputQuery(NamedTuple):
 class CfnLookup(LookupHandler):
     """CloudFormation Stack Output lookup."""
 
-    TYPE_NAME: Final[Literal["cfn"]] = "cfn"
+    TYPE_NAME: ClassVar[str] = "cfn"
     """Name that the Lookup is registered as."""
 
     @staticmethod
-    def should_use_provider(args: dict[str, str], provider: Provider | None) -> bool:
+    def should_use_provider(args: ParsedArgsTypeDef, provider: Provider | None) -> bool:
         """Determine if the provider should be used for the lookup.
 
         This will open happen when the lookup is used with CFNgin.
@@ -54,7 +54,7 @@ def should_use_provider(args: dict[str, str], provider: Provider | None) -> bool
 
         """
         if provider:
-            if args.get("region") and provider.region != args["region"]:
+            if "region" in args and provider.region != args["region"]:
                 LOGGER.debug("not using provider; requested region does not match")
                 return False
             LOGGER.debug("using provider")
diff --git a/runway/lookups/handlers/ecr.py b/runway/lookups/handlers/ecr.py
index f7d7bff27..b16624a3f 100644
--- a/runway/lookups/handlers/ecr.py
+++ b/runway/lookups/handlers/ecr.py
@@ -4,13 +4,12 @@
 
 import base64
 import logging
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ...lookups.handlers.base import LookupHandler
 
 if TYPE_CHECKING:
     from mypy_boto3_ecr.client import ECRClient
-    from typing_extensions import Literal
 
     from ...context import CfnginContext, RunwayContext
 
@@ -20,7 +19,7 @@
 class EcrLookup(LookupHandler):
     """ECR Lookup."""
 
-    TYPE_NAME: Final[Literal["ecr"]] = "ecr"
+    TYPE_NAME: ClassVar[str] = "ecr"
     """Name that the Lookup is registered as."""
 
     @staticmethod
diff --git a/runway/lookups/handlers/env.py b/runway/lookups/handlers/env.py
index 8398abb19..6eaa39422 100644
--- a/runway/lookups/handlers/env.py
+++ b/runway/lookups/handlers/env.py
@@ -3,12 +3,11 @@
 # pyright: reportIncompatibleMethodOverride=none
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from .base import LookupHandler
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ...context import CfnginContext, RunwayContext
 
@@ -16,7 +15,7 @@
 class EnvLookup(LookupHandler):
     """Environment variable Lookup."""
 
-    TYPE_NAME: Final[Literal["env"]] = "env"
+    TYPE_NAME: ClassVar[str] = "env"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/lookups/handlers/random_string.py b/runway/lookups/handlers/random_string.py
index b9811a44b..d48aa2599 100644
--- a/runway/lookups/handlers/random_string.py
+++ b/runway/lookups/handlers/random_string.py
@@ -6,7 +6,7 @@
 import logging
 import secrets
 import string
-from typing import TYPE_CHECKING, Any, Callable, Final
+from typing import TYPE_CHECKING, Any, Callable, ClassVar
 
 from ...utils import BaseModel
 from .base import LookupHandler
@@ -14,8 +14,6 @@
 if TYPE_CHECKING:
     from collections.abc import Sequence
 
-    from typing_extensions import Literal
-
     from ...context import CfnginContext, RunwayContext
 
 LOGGER = logging.getLogger(__name__)
@@ -33,7 +31,7 @@ class ArgsDataModel(BaseModel):
 class RandomStringLookup(LookupHandler):
     """Random string lookup."""
 
-    TYPE_NAME: Final[Literal["random.string"]] = "random.string"
+    TYPE_NAME: ClassVar[str] = "random.string"
     """Name that the Lookup is registered as."""
 
     @staticmethod
@@ -117,7 +115,7 @@ def handle(
         """
         raw_length, raw_args = cls.parse(value)
         length = int(raw_length)
-        args = ArgsDataModel.parse_obj(raw_args)
+        args = ArgsDataModel.model_validate(raw_args)
         char_set = cls.calculate_char_set(args)
         while True:
             result = cls.generate_random_string(char_set, length)
diff --git a/runway/lookups/handlers/ssm.py b/runway/lookups/handlers/ssm.py
index d34edd27f..76ccc469e 100644
--- a/runway/lookups/handlers/ssm.py
+++ b/runway/lookups/handlers/ssm.py
@@ -3,12 +3,12 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ...lookups.handlers.base import LookupHandler
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
+    from mypy_boto3_ssm.type_defs import ParameterTypeDef
 
     from ...context import CfnginContext, RunwayContext
 
@@ -18,14 +18,14 @@
 class SsmLookup(LookupHandler):
     """SSM Parameter Store Lookup."""
 
-    TYPE_NAME: Final[Literal["ssm"]] = "ssm"
+    TYPE_NAME: ClassVar[str] = "ssm"
     """Name that the Lookup is registered as."""
 
     @classmethod
     def handle(
         cls,
         value: str,
-        context: Union[CfnginContext, RunwayContext],
+        context: CfnginContext | RunwayContext,
         *__args: Any,
         **__kwargs: Any,
     ) -> Any:
@@ -46,12 +46,9 @@ def handle(
         client = session.client("ssm")
 
         try:
-            response = client.get_parameter(Name=query, WithDecryption=True)["Parameter"]
             return cls.format_results(
-                (
-                    response["Value"].split(",")
-                    if response["Type"] == "StringList"
-                    else response["Value"]
+                cls._handle_get_parameter(
+                    client.get_parameter(Name=query, WithDecryption=True)["Parameter"]
                 ),
                 **args,
             )
@@ -60,3 +57,13 @@ def handle(
                 args.pop("load", None)  # don't load a default value
                 return cls.format_results(args.pop("default"), **args)
             raise
+
+    @staticmethod
+    def _handle_get_parameter(parameter: ParameterTypeDef) -> list[str] | str | None:
+        """Handle the return value of ``get_parameter``."""
+        if "Value" not in parameter:
+            return None
+        value = parameter["Value"]
+        if parameter.get("Type") == "StringList":
+            return value.split(",")
+        return value
diff --git a/runway/lookups/handlers/var.py b/runway/lookups/handlers/var.py
index c33499dee..988eabbb1 100644
--- a/runway/lookups/handlers/var.py
+++ b/runway/lookups/handlers/var.py
@@ -4,12 +4,11 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Final
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from .base import LookupHandler
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
 
     from ...utils import MutableMap
 
@@ -21,7 +20,7 @@
 class VarLookup(LookupHandler):
     """Variable definition Lookup."""
 
-    TYPE_NAME: Final[Literal["var"]] = "var"
+    TYPE_NAME: ClassVar[str] = "var"
     """Name that the Lookup is registered as."""
 
     @classmethod
diff --git a/runway/module/base.py b/runway/module/base.py
index 97350e4c8..3f59f639d 100644
--- a/runway/module/base.py
+++ b/runway/module/base.py
@@ -4,7 +4,7 @@
 
 import logging
 import subprocess
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
 
 from ..exceptions import NpmNotFound
 from ..utils import which
@@ -19,14 +19,31 @@
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__))
 
 
-class RunwayModule:
+class ModuleOptions:
+    """Base class for Runway module options."""
+
+    def get(self, name: str, default: Any = None) -> Any:
+        """Get a value or return the default."""
+        return getattr(self, name, default)
+
+    def __eq__(self, other: object) -> bool:
+        """Assess equality."""
+        if isinstance(other, self.__class__):
+            return self.__dict__ == other.__dict__
+        return False
+
+
+_ModuleOptionsTypeVar = TypeVar("_ModuleOptionsTypeVar", bound="ModuleOptions | dict[str, Any]")
+
+
+class RunwayModule(Generic[_ModuleOptionsTypeVar]):
     """Base class for Runway modules."""
 
     ctx: RunwayContext
     explicitly_enabled: bool | None
     logger: PrefixAdaptor | RunwayLogger
     name: str
-    options: dict[str, Any] | ModuleOptions
+    options: _ModuleOptionsTypeVar
     region: str
 
     def __init__(
@@ -37,7 +54,7 @@ def __init__(
         logger: RunwayLogger = LOGGER,
         module_root: Path,
         name: str | None = None,
-        options: dict[str, Any] | ModuleOptions | None = None,
+        options: _ModuleOptionsTypeVar | None = None,
         parameters: dict[str, Any] | None = None,
         **_: Any,
     ) -> None:
@@ -93,7 +110,7 @@ def __getitem__(self, key: str) -> Any:
         return getattr(self, key)
 
 
-class RunwayModuleNpm(RunwayModule):
+class RunwayModuleNpm(RunwayModule[_ModuleOptionsTypeVar]):
     """Base class for Runway modules that use npm."""
 
     def __init__(
@@ -104,7 +121,7 @@ def __init__(
         logger: RunwayLogger = LOGGER,
         module_root: Path,
         name: str | None = None,
-        options: dict[str, Any] | ModuleOptions | None = None,
+        options: _ModuleOptionsTypeVar | None = None,
         parameters: dict[str, Any] | None = None,
         **_: Any,
     ) -> None:
@@ -209,17 +226,3 @@ def warn_on_boto_env_vars(
                 "during use of nodejs-based module and AWS_PROFILE is "
                 "not set -- you likely want to set AWS_PROFILE instead"
             )
-
-
-class ModuleOptions:
-    """Base class for Runway module options."""
-
-    def get(self, name: str, default: Any = None) -> Any:
-        """Get a value or return the default."""
-        return getattr(self, name, default)
-
-    def __eq__(self, other: object) -> bool:
-        """Assess equality."""
-        if isinstance(other, self.__class__):
-            return self.__dict__ == other.__dict__
-        return False
diff --git a/runway/module/cdk.py b/runway/module/cdk.py
index df3ea2c0a..65c066e5b 100644
--- a/runway/module/cdk.py
+++ b/runway/module/cdk.py
@@ -40,7 +40,41 @@
 ]
 
 
-class CloudDevelopmentKit(RunwayModuleNpm):
+class CloudDevelopmentKitOptions(ModuleOptions):
+    """Module options for AWS Cloud Development Kit.
+
+    Attributes:
+        build_steps: A list of commands to be executed before each action (e.g.
+            diff, deploy, destroy).
+        data: Options parsed into a data model.
+        skip_npm_ci: Skip running ``npm ci`` in the module directory prior to
+            processing the module.
+
+    """
+
+    def __init__(self, data: RunwayCdkModuleOptionsDataModel) -> None:
+        """Instantiate class.
+
+        Args:
+            data: Options parsed into a data model.
+
+        """
+        self.build_steps = data.build_steps
+        self.data = data
+        self.skip_npm_ci = data.skip_npm_ci
+
+    @classmethod
+    def parse_obj(cls, obj: object) -> CloudDevelopmentKitOptions:
+        """Parse options definition and return an options object.
+
+        Args:
+            obj: Object to parse.
+
+        """
+        return cls(data=RunwayCdkModuleOptionsDataModel.model_validate(obj))
+
+
+class CloudDevelopmentKit(RunwayModuleNpm[CloudDevelopmentKitOptions]):
     """CDK Runway Module."""
 
     DEPRECATION_MSG = (
@@ -48,8 +82,6 @@ class CloudDevelopmentKit(RunwayModuleNpm):
         "may be removed in the next major release."
     )
 
-    options: CloudDevelopmentKitOptions
-
     def __init__(
         self,
         context: RunwayContext,
@@ -284,37 +316,3 @@ def run_build_steps(self) -> None:
                 )
                 raise
         self.logger.info("build steps (complete)")
-
-
-class CloudDevelopmentKitOptions(ModuleOptions):
-    """Module options for AWS Cloud Development Kit.
-
-    Attributes:
-        build_steps: A list of commands to be executed before each action (e.g.
-            diff, deploy, destroy).
-        data: Options parsed into a data model.
-        skip_npm_ci: Skip running ``npm ci`` in the module directory prior to
-            processing the module.
-
-    """
-
-    def __init__(self, data: RunwayCdkModuleOptionsDataModel) -> None:
-        """Instantiate class.
-
-        Args:
-            data: Options parsed into a data model.
-
-        """
-        self.build_steps = data.build_steps
-        self.data = data
-        self.skip_npm_ci = data.skip_npm_ci
-
-    @classmethod
-    def parse_obj(cls, obj: object) -> CloudDevelopmentKitOptions:
-        """Parse options definition and return an options object.
-
-        Args:
-            obj: Object to parse.
-
-        """
-        return cls(data=RunwayCdkModuleOptionsDataModel.parse_obj(obj))
diff --git a/runway/module/cloudformation.py b/runway/module/cloudformation.py
index e46ef24e0..3f943f215 100644
--- a/runway/module/cloudformation.py
+++ b/runway/module/cloudformation.py
@@ -19,7 +19,7 @@
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__))
 
 
-class CloudFormation(RunwayModule):
+class CloudFormation(RunwayModule["ModuleOptions | dict[str, Any]"]):
     """CloudFormation (CFNgin) Runway Module."""
 
     def __init__(
diff --git a/runway/module/k8s.py b/runway/module/k8s.py
index 901692e4c..acfe30eda 100644
--- a/runway/module/k8s.py
+++ b/runway/module/k8s.py
@@ -49,7 +49,103 @@
 ]
 
 
-class K8s(RunwayModule):
+class K8sOptions(ModuleOptions):
+    """Module options for Kubernetes.
+
+    Attributes:
+        data: Options parsed into a data model.
+        deploy_environment: Runway deploy environment object.
+        kubectl_version: Version of kubectl to use.
+        path: Module path.
+
+    """
+
+    data: RunwayK8sModuleOptionsDataModel
+    deploy_environment: DeployEnvironment
+    kubectl_version: str | None
+    path: Path
+
+    def __init__(
+        self,
+        data: RunwayK8sModuleOptionsDataModel,
+        deploy_environment: DeployEnvironment,
+        path: Path,
+    ) -> None:
+        """Instantiate class.
+
+        Args:
+            data: Options parsed into a data model.
+            deploy_environment: Current deploy environment.
+            path: Module path.
+
+        """
+        self.data = data
+        self.env = deploy_environment
+        self.kubectl_version = data.kubectl_version
+        self.path = path
+
+    @cached_property
+    def kustomize_config(self) -> Path:
+        """Kustomize configuration file."""
+        return self.overlay_path / "kustomization.yaml"
+
+    @cached_property
+    def overlay_path(self) -> Path:
+        """Directory containing the kustomize overlay to use."""
+        if self.data.overlay_path:
+            return self.data.overlay_path
+        return self.get_overlay_dir(
+            path=self.path / "overlays",
+            environment=self.env.name,
+            region=self.env.aws_region,
+        )
+
+    @staticmethod
+    def gen_overlay_dirs(environment: str, region: str) -> list[str]:
+        """Generate possible overlay directories.
+
+        Prefers more explicit directory name but falls back to environment name only.
+
+        Args:
+            environment: Current deploy environment.
+            region : Current AWS region.
+
+        """
+        return [f"{environment}-{region}", environment]
+
+    @classmethod
+    def get_overlay_dir(cls, path: Path, environment: str, region: str) -> Path:
+        """Determine the overlay directory to use."""
+        overlay_dir = path
+        for name in cls.gen_overlay_dirs(environment, region):
+            overlay_dir = path / name
+            if (overlay_dir / "kustomization.yaml").is_file():
+                return overlay_dir
+        return overlay_dir
+
+    @classmethod
+    def parse_obj(
+        cls,
+        deploy_environment: DeployEnvironment,
+        obj: object,
+        path: Path | None = None,
+    ) -> K8sOptions:
+        """Parse options definition and return an options object.
+
+        Args:
+            deploy_environment: Current deploy environment.
+            obj: Object to parse.
+            path: Module path.
+
+        """
+        return cls(
+            data=RunwayK8sModuleOptionsDataModel.model_validate(obj),
+            deploy_environment=deploy_environment,
+            path=path or Path.cwd(),
+        )
+
+
+class K8s(RunwayModule[K8sOptions]):
     """Kubectl Runway Module."""
 
     DEPRECATION_MSG = (
@@ -57,8 +153,6 @@ class K8s(RunwayModule):
         "may be removed in the next major release."
     )
 
-    options: K8sOptions
-
     def __init__(
         self,
         context: RunwayContext,
@@ -228,99 +322,3 @@ def plan(self) -> None:
         if self.skip:
             return
         self.logger.info("kustomized yaml generated by kubectl:\n\n%s", self.kubectl_kustomize())
-
-
-class K8sOptions(ModuleOptions):
-    """Module options for Kubernetes.
-
-    Attributes:
-        data: Options parsed into a data model.
-        deploy_environment: Runway deploy environment object.
-        kubectl_version: Version of kubectl to use.
-        path: Module path.
-
-    """
-
-    data: RunwayK8sModuleOptionsDataModel
-    deploy_environment: DeployEnvironment
-    kubectl_version: str | None
-    path: Path
-
-    def __init__(
-        self,
-        data: RunwayK8sModuleOptionsDataModel,
-        deploy_environment: DeployEnvironment,
-        path: Path,
-    ) -> None:
-        """Instantiate class.
-
-        Args:
-            data: Options parsed into a data model.
-            deploy_environment: Current deploy environment.
-            path: Module path.
-
-        """
-        self.data = data
-        self.env = deploy_environment
-        self.kubectl_version = data.kubectl_version
-        self.path = path
-
-    @cached_property
-    def kustomize_config(self) -> Path:
-        """Kustomize configuration file."""
-        return self.overlay_path / "kustomization.yaml"
-
-    @cached_property
-    def overlay_path(self) -> Path:
-        """Directory containing the kustomize overlay to use."""
-        if self.data.overlay_path:
-            return self.data.overlay_path
-        return self.get_overlay_dir(
-            path=self.path / "overlays",
-            environment=self.env.name,
-            region=self.env.aws_region,
-        )
-
-    @staticmethod
-    def gen_overlay_dirs(environment: str, region: str) -> list[str]:
-        """Generate possible overlay directories.
-
-        Prefers more explicit directory name but falls back to environment name only.
-
-        Args:
-            environment: Current deploy environment.
-            region : Current AWS region.
-
-        """
-        return [f"{environment}-{region}", environment]
-
-    @classmethod
-    def get_overlay_dir(cls, path: Path, environment: str, region: str) -> Path:
-        """Determine the overlay directory to use."""
-        overlay_dir = path
-        for name in cls.gen_overlay_dirs(environment, region):
-            overlay_dir = path / name
-            if (overlay_dir / "kustomization.yaml").is_file():
-                return overlay_dir
-        return overlay_dir
-
-    @classmethod
-    def parse_obj(
-        cls,
-        deploy_environment: DeployEnvironment,
-        obj: object,
-        path: Path | None = None,
-    ) -> K8sOptions:
-        """Parse options definition and return an options object.
-
-        Args:
-            deploy_environment: Current deploy environment.
-            obj: Object to parse.
-            path: Module path.
-
-        """
-        return cls(
-            data=RunwayK8sModuleOptionsDataModel.parse_obj(obj),
-            deploy_environment=deploy_environment,
-            path=path or Path.cwd(),
-        )
diff --git a/runway/module/serverless.py b/runway/module/serverless.py
index 8630e91cc..5d44cd395 100644
--- a/runway/module/serverless.py
+++ b/runway/module/serverless.py
@@ -47,10 +47,89 @@ def gen_sls_config_files(stage: str, region: str) -> list[str]:
     return names
 
 
-class Serverless(RunwayModuleNpm):
-    """Serverless Runway Module."""
+class ServerlessOptions(ModuleOptions):
+    """Module options for Serverless Framework.
 
-    options: ServerlessOptions
+    Attributes:
+        data: Options parsed into a data model.
+        extend_serverless_yml: If provided, the value of this option will be
+            recursively merged into the module's Serverless config file.
+        promotezip: If provided, promote Serverless Framework generated zip files
+            between environments from a build AWS account.
+        skip_npm_ci: Skip running ``npm ci`` in the module directory prior to
+            processing the module.
+
+    """
+
+    def __init__(self, data: RunwayServerlessModuleOptionsDataModel) -> None:
+        """Instantiate class.
+
+        Args:
+            data: Options parsed into a data model.
+
+        """
+        self._arg_parser = self._create_arg_parser()
+        cli_args, self._unknown_cli_args = self._arg_parser.parse_known_args(data.args.copy())
+        self._cli_args = vars(cli_args)  # convert argparse.Namespace to dict
+
+        self.data = data
+        self.extend_serverless_yml = data.extend_serverless_yml
+        self.promotezip = data.promotezip
+        self.skip_npm_ci = data.skip_npm_ci
+
+    @property
+    def args(self) -> list[str]:
+        """List of CLI arguments/options to pass to the Serverless Framework CLI."""
+        known_args: list[str] = []
+        for key, val in self._cli_args.items():
+            if isinstance(val, str):
+                known_args.extend([f"--{key}", val])
+        return known_args + self._unknown_cli_args
+
+    def update_args(self, key: str, value: str) -> None:
+        """Update a known CLI argument.
+
+        Args:
+            key: Dict key to be updated.
+            value: New value
+
+        Raises:
+            KeyError: The key provided for update is not a known arg.
+
+        """
+        if key in self._cli_args:
+            self._cli_args[key] = value
+        else:
+            raise KeyError(key)
+
+    @staticmethod
+    def _create_arg_parser() -> argparse.ArgumentParser:
+        """Create argparse parser to parse args.
+
+        Used to pull arguments out of self.args when logic could change
+        depending on values provided.
+
+        Returns:
+            argparse.ArgumentParser
+
+        """
+        parser = argparse.ArgumentParser()
+        parser.add_argument("-c", "--config", default=None)
+        return parser
+
+    @classmethod
+    def parse_obj(cls, obj: object) -> ServerlessOptions:
+        """Parse options definition and return an options object.
+
+        Args:
+            obj: Object to parse.
+
+        """
+        return cls(data=RunwayServerlessModuleOptionsDataModel.model_validate(obj))
+
+
+class Serverless(RunwayModuleNpm[ServerlessOptions]):
+    """Serverless Runway Module."""
 
     def __init__(
         self,
@@ -395,7 +474,7 @@ def source_hash(self) -> dict[str, str]:
                 )
                 for name, detail in self.config.get("functions", {}).items()
             }
-        directories: list[dict[str, list[str] | str]] = []
+        directories: list[dict[str, list[str] | str | None]] | None = []
         for detail in self.config.get("functions", {}).values():
             func_path = {"path": os.path.dirname(detail.get("handler"))}  # noqa: PTH120
             if func_path not in directories:
@@ -440,84 +519,3 @@ def sync_with_s3(self, bucket_name: str) -> None:
                     filename=str(file_path),
                     session=session,
                 )
-
-
-class ServerlessOptions(ModuleOptions):
-    """Module options for Serverless Framework.
-
-    Attributes:
-        data: Options parsed into a data model.
-        extend_serverless_yml: If provided, the value of this option will be
-            recursively merged into the module's Serverless config file.
-        promotezip: If provided, promote Serverless Framework generated zip files
-            between environments from a build AWS account.
-        skip_npm_ci: Skip running ``npm ci`` in the module directory prior to
-            processing the module.
-
-    """
-
-    def __init__(self, data: RunwayServerlessModuleOptionsDataModel) -> None:
-        """Instantiate class.
-
-        Args:
-            data: Options parsed into a data model.
-
-        """
-        self._arg_parser = self._create_arg_parser()
-        cli_args, self._unknown_cli_args = self._arg_parser.parse_known_args(data.args.copy())
-        self._cli_args = vars(cli_args)  # convert argparse.Namespace to dict
-
-        self.data = data
-        self.extend_serverless_yml = data.extend_serverless_yml
-        self.promotezip = data.promotezip
-        self.skip_npm_ci = data.skip_npm_ci
-
-    @property
-    def args(self) -> list[str]:
-        """List of CLI arguments/options to pass to the Serverless Framework CLI."""
-        known_args: list[str] = []
-        for key, val in self._cli_args.items():
-            if isinstance(val, str):
-                known_args.extend([f"--{key}", val])
-        return known_args + self._unknown_cli_args
-
-    def update_args(self, key: str, value: str) -> None:
-        """Update a known CLI argument.
-
-        Args:
-            key: Dict key to be updated.
-            value: New value
-
-        Raises:
-            KeyError: The key provided for update is not a known arg.
-
-        """
-        if key in self._cli_args:
-            self._cli_args[key] = value
-        else:
-            raise KeyError(key)
-
-    @staticmethod
-    def _create_arg_parser() -> argparse.ArgumentParser:
-        """Create argparse parser to parse args.
-
-        Used to pull arguments out of self.args when logic could change
-        depending on values provided.
-
-        Returns:
-            argparse.ArgumentParser
-
-        """
-        parser = argparse.ArgumentParser()
-        parser.add_argument("-c", "--config", default=None)
-        return parser
-
-    @classmethod
-    def parse_obj(cls, obj: object) -> ServerlessOptions:
-        """Parse options definition and return an options object.
-
-        Args:
-            obj: Object to parse.
-
-        """
-        return cls(data=RunwayServerlessModuleOptionsDataModel.parse_obj(obj))
diff --git a/runway/module/staticsite/handler.py b/runway/module/staticsite/handler.py
index 7dab0d2c6..fdcbbd077 100644
--- a/runway/module/staticsite/handler.py
+++ b/runway/module/staticsite/handler.py
@@ -29,7 +29,7 @@
 LOGGER = cast("RunwayLogger", logging.getLogger(__name__))
 
 
-class StaticSite(RunwayModule):
+class StaticSite(RunwayModule[StaticSiteOptions]):
     """Static website Runway Module."""
 
     DEPRECATION_MSG = (
@@ -37,7 +37,6 @@ class StaticSite(RunwayModule):
         "may be removed in the next major release."
     )
 
-    options: StaticSiteOptions
     parameters: RunwayStaticSiteModuleParametersDataModel
 
     def __init__(
@@ -78,7 +77,7 @@ def __init__(
             options=StaticSiteOptions.parse_obj(options or {}),
             parameters=parameters,
         )
-        self.parameters = RunwayStaticSiteModuleParametersDataModel.parse_obj(self.parameters)
+        self.parameters = RunwayStaticSiteModuleParametersDataModel.model_validate(self.parameters)
         # logger needs to be created here to use the correct logger
         self.logger = PrefixAdaptor(self.name, LOGGER)
         LOGGER.warning("%s:%s", self.name, self.DEPRECATION_MSG)
@@ -164,7 +163,7 @@ def _setup_website_module(self, command: str) -> None:
         # have any costs when unused.
         if command == "destroy" and (
             self.parameters.auth_at_edge
-            or self.parameters.dict().get("staticsite_rewrite_index_index")
+            or self.parameters.model_dump().get("staticsite_rewrite_index_index")
         ):
             self._create_cleanup_yaml(module_dir)
 
@@ -173,8 +172,8 @@ def _setup_website_module(self, command: str) -> None:
             explicitly_enabled=self.explicitly_enabled,
             module_root=module_dir,
             name=self.name,
-            options=self.options.data.dict(),
-            parameters=self.parameters.dict(by_alias=True),
+            options=self.options.data.model_dump(),
+            parameters=self.parameters.model_dump(by_alias=True),
         )
         self.logger.info("%s (in progress)", command)
         getattr(cfn, command)()
@@ -300,7 +299,7 @@ def _create_staticsite_yaml(self, module_dir: Path) -> Path:
 
         build_staticsite_args: dict[str, Any] = {
             # ensures yaml.safe_load will work by using JSON to convert objects
-            "options": json.loads(self.options.data.json(by_alias=True))
+            "options": json.loads(self.options.data.model_dump_json(by_alias=True))
         }
         build_staticsite_args["artifact_bucket_rxref_lookup"] = (
             f"{self.sanitized_name}-dependencies::ArtifactsBucketName"
@@ -331,7 +330,7 @@ def _create_staticsite_yaml(self, module_dir: Path) -> Path:
                     "CFDistributionDomainName::default=undefined}",
                     "distribution_id": f"${{cfn ${{namespace}}-{self.sanitized_name}"
                     ".CFDistributionId::default=undefined}",
-                    "extra_files": [i.dict() for i in self.options.extra_files],
+                    "extra_files": [i.model_dump() for i in self.options.extra_files],
                     "website_url": f"${{cfn ${{namespace}}-{self.sanitized_name}"
                     ".BucketWebsiteURL::default=undefined}",
                 },
@@ -419,13 +418,13 @@ def _create_staticsite_yaml(self, module_dir: Path) -> Path:
             site_stack_variables["RoleBoundaryArn"] = self.parameters.role_boundary_arn
 
         site_stack_variables["custom_error_responses"] = [
-            i.dict(exclude_none=True) for i in self.parameters.custom_error_responses
+            i.model_dump(exclude_none=True) for i in self.parameters.custom_error_responses
         ]
         site_stack_variables["lambda_function_associations"] = [
-            i.dict() for i in self.parameters.lambda_function_associations
+            i.model_dump() for i in self.parameters.lambda_function_associations
         ]
 
-        content = {
+        content: dict[str, Any] = {
             "cfngin_bucket": "",
             "namespace": "${namespace}",
             "post_deploy": post_deploy,
@@ -515,10 +514,10 @@ def _get_site_stack_variables(self) -> dict[str, Any]:
             site_stack_variables["OAuthScopes"] = self.parameters.oauth_scopes
         else:
             site_stack_variables["custom_error_responses"] = [
-                i.dict(exclude_none=True) for i in self.parameters.custom_error_responses
+                i.model_dump(exclude_none=True) for i in self.parameters.custom_error_responses
             ]
             site_stack_variables["lambda_function_associations"] = [
-                i.dict() for i in self.parameters.lambda_function_associations
+                i.model_dump() for i in self.parameters.lambda_function_associations
             ]
 
         return site_stack_variables
diff --git a/runway/module/staticsite/options/__init__.py b/runway/module/staticsite/options/__init__.py
index 373c98c85..254648e35 100644
--- a/runway/module/staticsite/options/__init__.py
+++ b/runway/module/staticsite/options/__init__.py
@@ -1,7 +1,7 @@
 """Runway Static Site Module options."""
 
-from .components import StaticSiteOptions
-from .models import (
+from ._components import StaticSiteOptions
+from ._models import (
     RunwayStaticSiteExtraFileDataModel,
     RunwayStaticSiteModuleOptionsDataModel,
     RunwayStaticSitePreBuildStepDataModel,
diff --git a/runway/module/staticsite/options/components.py b/runway/module/staticsite/options/_components.py
similarity index 90%
rename from runway/module/staticsite/options/components.py
rename to runway/module/staticsite/options/_components.py
index 642a0c859..5f735ef2d 100644
--- a/runway/module/staticsite/options/components.py
+++ b/runway/module/staticsite/options/_components.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 from ...base import ModuleOptions
-from .models import RunwayStaticSiteModuleOptionsDataModel
+from ._models import RunwayStaticSiteModuleOptionsDataModel
 
 
 class StaticSiteOptions(ModuleOptions):
@@ -38,4 +38,4 @@ def parse_obj(cls, obj: object) -> StaticSiteOptions:
             obj: Object to parse.
 
         """
-        return cls(data=RunwayStaticSiteModuleOptionsDataModel.parse_obj(obj))
+        return cls(data=RunwayStaticSiteModuleOptionsDataModel.model_validate(obj))
diff --git a/runway/module/staticsite/options/_models.py b/runway/module/staticsite/options/_models.py
new file mode 100644
index 000000000..29065508d
--- /dev/null
+++ b/runway/module/staticsite/options/_models.py
@@ -0,0 +1,158 @@
+"""Runway static site Module options."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any, cast
+
+from pydantic import ConfigDict, model_validator
+
+from ....config.models.base import ConfigProperty
+
+
+class RunwayStaticSiteExtraFileDataModel(ConfigProperty):
+    """Model for Runway static site Module extra_files option item."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module extra_files option item",
+        validate_assignment=True,
+        validate_default=True,
+    )
+
+    content_type: str | None = None
+    """An explicit content type for the file.
+    If not provided, will attempt to determine based on the name provided.
+
+    """
+
+    content: Any = None
+    """Inline content that will be used as the file content.
+    This or ``file`` must be provided.
+
+    """
+
+    file: Path | None = None
+    """Path to an existing file.
+    The content of this file will be uploaded to the static site S3 bucket using
+    the name as the object key.
+    This or ``content`` must be provided.
+
+    """
+
+    name: str
+    """The destination name of the file to create."""
+
+    @model_validator(mode="before")
+    @classmethod
+    def _autofill_content_type(cls, values: dict[str, Any]) -> dict[str, Any]:
+        """Attempt to fill content_type if not provided."""
+        if values.get("content_type"):
+            return values
+        name = cast(str, values.get("name", ""))
+        if name.endswith(".json"):
+            values["content_type"] = "application/json"
+        elif name.endswith((".yaml", ".yml")):
+            values["content_type"] = "text/yaml"
+        return values
+
+    @model_validator(mode="before")
+    @classmethod
+    def _validate_content_or_file(cls, values: dict[str, Any]) -> dict[str, Any]:
+        """Validate that content or file is provided."""
+        if all(i in values and values[i] for i in ["content", "file"]):
+            raise ValueError("only one of content or file can be provided")
+        if not any(i in values for i in ["content", "file"]):
+            raise ValueError("one of content or file must be provided")
+        return values
+
+
+class RunwayStaticSitePreBuildStepDataModel(ConfigProperty):
+    """Model for Runway static site Module pre_build_steps option item."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module pre_build_steps option item",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    command: str
+    """The command to run."""
+
+    cwd: Path = Path.cwd()
+    """The working directory for the subprocess running the command.
+    If not provided, the current working directory is used.
+
+    """
+
+
+class RunwayStaticSiteSourceHashingDirectoryDataModel(ConfigProperty):
+    """Model for Runway static site Module source_hashing.directory option item."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module source_hashing.directories option item",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    exclusions: list[str] = []
+    """List of gitignore formmated globs to ignore when calculating the hash."""
+
+    path: Path
+    """Path to files to include in the hash."""
+
+
+class RunwayStaticSiteSourceHashingDataModel(ConfigProperty):
+    """Model for Runway static site Module source_hashing option."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module source_hashing option",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    directories: list[RunwayStaticSiteSourceHashingDirectoryDataModel] = [
+        RunwayStaticSiteSourceHashingDirectoryDataModel(path="./")  # type: ignore
+    ]
+    """Explicitly provide the directories to use when calculating the hash.
+    If not provided, will default to the root of the module.
+    """
+
+    enabled: bool = True
+    """Enable source hashing. If not enabled, build and upload will occur on every deploy."""
+
+    parameter: str | None = None
+    """SSM parameter where the hash of each build is stored."""
+
+
+class RunwayStaticSiteModuleOptionsDataModel(ConfigProperty):
+    """Model for Runway static site Module options."""
+
+    model_config = ConfigDict(
+        extra="ignore",
+        title="Runway static site Module options",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    build_output: str = "./"
+    """Directory where build output is placed. Defaults to current working directory."""
+
+    build_steps: list[str] = []
+    """List of commands to run to build the static site."""
+
+    extra_files: list[RunwayStaticSiteExtraFileDataModel] = []
+    """List of files that should be uploaded to S3 after the build.
+    Used to dynamically create or select file.
+    """
+
+    pre_build_steps: list[RunwayStaticSitePreBuildStepDataModel] = []
+    """Commands to be run prior to the build process."""
+
+    source_hashing: RunwayStaticSiteSourceHashingDataModel = (
+        RunwayStaticSiteSourceHashingDataModel()
+    )
+    """Overrides for source hash calculation and tracking."""
diff --git a/runway/module/staticsite/options/models.py b/runway/module/staticsite/options/models.py
deleted file mode 100644
index f30aa1e52..000000000
--- a/runway/module/staticsite/options/models.py
+++ /dev/null
@@ -1,153 +0,0 @@
-"""Runway static site Module options."""
-
-# ruff: noqa: UP006, UP035
-from __future__ import annotations
-
-from pathlib import Path
-from typing import Any, List, Optional, cast
-
-from pydantic import Extra, root_validator
-
-from ....config.models.base import ConfigProperty
-
-
-class RunwayStaticSiteExtraFileDataModel(ConfigProperty):
-    """Model for Runway static site Module extra_files option item.
-
-    Attributes:
-        content_type: An explicit content type for the file. If not provided,
-            will attempt to determine based on the name provided.
-        content: Inline content that will be used as the file content.
-            This or ``file`` must be provided.
-        file: Path to an existing file. The content of this file will be uploaded
-            to the static site S3 bucket using the name as the object key.
-            This or ``content`` must be provided.
-        name: The destination name of the file to create.
-
-    """
-
-    content_type: Optional[str] = None
-    content: Any = None
-    file: Optional[Path] = None
-    name: str
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module extra_files option item."
-
-    @root_validator  # type: ignore
-    def _autofill_content_type(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
-        """Attempt to fill content_type if not provided."""
-        if values.get("content_type"):
-            return values
-        name = cast(str, values.get("name", ""))
-        if name.endswith(".json"):
-            values["content_type"] = "application/json"
-        elif name.endswith((".yaml", ".yml")):
-            values["content_type"] = "text/yaml"
-        return values
-
-    @root_validator(pre=True)  # type: ignore
-    def _validate_content_or_file(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
-        """Validate that content or file is provided."""
-        if all(i in values and values[i] for i in ["content", "file"]):
-            raise ValueError("only one of content or file can be provided")
-        if not any(i in values for i in ["content", "file"]):
-            raise ValueError("one of content or file must be provided")
-        return values
-
-
-class RunwayStaticSitePreBuildStepDataModel(ConfigProperty):
-    """Model for Runway static site Module pre_build_steps option item.
-
-    Attributes:
-        command: The command to run.
-        cwd: The working directory for the subprocess running the command.
-            If not provided, the current working directory is used.
-
-    """
-
-    command: str
-    cwd: Path = Path.cwd()
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module pre_build_steps option item."
-
-
-class RunwayStaticSiteSourceHashingDirectoryDataModel(ConfigProperty):
-    """Model for Runway static site Module source_hashing.directory option item.
-
-    Attributes:
-        exclusions: List of gitignore formatted globs to ignore when calculating
-            the hash.
-        path: Path to files to include in the hash.
-
-    """
-
-    exclusions: List[str] = []
-    path: Path
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module source_hashing.directories option item."
-
-
-class RunwayStaticSiteSourceHashingDataModel(ConfigProperty):
-    """Model for Runway static site Module source_hashing option.
-
-    Attributes:
-        directories: Explicitly provide the directories to use when calculating
-            the hash. If not provided, will default to the root of the module.
-        enabled: Enable source hashing. If not enabled, build and upload will
-            occur on every deploy.
-        parameter: SSM parameter where the hash of each build is stored.
-
-    """
-
-    directories: List[RunwayStaticSiteSourceHashingDirectoryDataModel] = [
-        RunwayStaticSiteSourceHashingDirectoryDataModel(path="./")  # type: ignore
-    ]
-    enabled: bool = True
-    parameter: Optional[str] = None
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module source_hashing option."
-
-
-class RunwayStaticSiteModuleOptionsDataModel(ConfigProperty):
-    """Model for Runway static site Module options.
-
-    Attributes:
-        build_output: Directory where build output is placed. Defaults to current
-            working directory.
-        build_steps: List of commands to run to build the static site.
-        extra_files: List of files that should be uploaded to S3 after the build.
-            Used to dynamically create or select file.
-        pre_build_steps: Commands to be run prior to the build process.
-        source_hashing: Overrides for source hash calculation and tracking.
-
-    """
-
-    build_output: str = "./"
-    build_steps: List[str] = []
-    extra_files: List[RunwayStaticSiteExtraFileDataModel] = []
-    pre_build_steps: List[RunwayStaticSitePreBuildStepDataModel] = []
-    source_hashing: RunwayStaticSiteSourceHashingDataModel = (
-        RunwayStaticSiteSourceHashingDataModel()
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.ignore
-        title = "Runway static site Module options."
diff --git a/runway/module/staticsite/parameters/__init__.py b/runway/module/staticsite/parameters/__init__.py
index 89af619c2..ee7220cda 100644
--- a/runway/module/staticsite/parameters/__init__.py
+++ b/runway/module/staticsite/parameters/__init__.py
@@ -1,6 +1,6 @@
 """Runway Static Site Module parameters."""
 
-from .models import (
+from ._models import (
     RunwayStaticSiteCustomErrorResponseDataModel,
     RunwayStaticSiteLambdaFunctionAssociationDataModel,
     RunwayStaticSiteModuleParametersDataModel,
diff --git a/runway/module/staticsite/parameters/_models.py b/runway/module/staticsite/parameters/_models.py
new file mode 100644
index 000000000..a5a6bfc4e
--- /dev/null
+++ b/runway/module/staticsite/parameters/_models.py
@@ -0,0 +1,190 @@
+"""Runway static site Module parameters."""
+
+from __future__ import annotations
+
+from pydantic import ConfigDict, Field, field_validator
+
+from ....config.models.base import ConfigProperty
+
+
+class RunwayStaticSiteCustomErrorResponseDataModel(ConfigProperty):
+    """Model for Runway stat site Module staticsite_custom_error_responses parameter item.
+
+    https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-customerrorresponse.html
+
+    """
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module staticsite_custom_error_responses parameter item",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    ErrorCachingMinTTL: int | None = None
+    ErrorCode: int | None = None
+    ResponseCode: int | None = None
+    ResponsePagePath: str | None = None
+
+
+class RunwayStaticSiteLambdaFunctionAssociationDataModel(ConfigProperty):
+    """Model for Runway stat site Module staticsite_lambda_function_associations parameter item."""
+
+    model_config = ConfigDict(
+        extra="forbid",
+        title="Runway static site Module staticsite_lambda_function_associations parameter item",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    arn: str
+    """Lambda function ARN."""
+
+    type: str
+    """Association type."""
+
+
+def _staticsite_alias_generator(field_name: str) -> str:
+    """Append ``staticsite`` to field names.
+
+    Some fields are excluded from having aliases (e.g. namespace).
+
+    """
+    return f"staticsite_{field_name}" if field_name != "namespace" else field_name
+
+
+class RunwayStaticSiteModuleParametersDataModel(ConfigProperty):
+    """Model for Runway static site Module parameters."""
+
+    model_config = ConfigDict(
+        alias_generator=_staticsite_alias_generator,
+        extra="ignore",
+        populate_by_name=True,
+        title="Runway static site Module parameters",
+        validate_default=True,
+        validate_assignment=True,
+    )
+
+    acmcert_arn: str | None = None
+    """The certificate arn used for any alias domains supplied.
+    This is a requirement when supplying any custom domain.
+
+    """
+
+    additional_redirect_domains: list[str] = []
+    """Additional domains (beyond the ``aliases`` domains or the CloudFront URL if
+    no aliases are provided) that will be authorized by the Auth@Edge UserPool AppClient.
+
+    """
+
+    aliases: list[str] = []
+    """Any custom domains that should be added to the CloudFront Distribution."""
+
+    auth_at_edge: bool = False
+    """Auth@Edge make the static site private by placing it behind an authorization wall."""
+
+    cf_disable: bool = False
+    """Whether deployment of the CloudFront Distribution should be disabled."""
+
+    compress: bool = True
+    """Whether the CloudFront default cache behavior will automatically compress certain files."""
+
+    cookie_settings: dict[str, str] = {
+        "idToken": "Path=/; Secure; SameSite=Lax",
+        "accessToken": "Path=/; Secure; SameSite=Lax",
+        "refreshToken": "Path=/; Secure; SameSite=Lax",
+        "nonce": "Path=/; Secure; HttpOnly; Max-Age=1800; SameSite=Lax",
+    }
+    """The default cookie settings for retrieved tokens and generated nonce's."""
+
+    create_user_pool: bool = False
+    """Whether to create a User Pool for the Auth@Edge configuration."""
+
+    custom_error_responses: list[RunwayStaticSiteCustomErrorResponseDataModel] = []
+    """Define custom error responses."""
+
+    enable_cf_logging: bool = True
+    """Enable CloudFront logging."""
+
+    http_headers: dict[str, str] = {
+        "Content-Security-Policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; "
+        "font-src 'self' 'unsafe-inline' 'unsafe-eval' data: https:; "
+        "object-src 'none'; "
+        "connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com",
+        "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+        "Referrer-Policy": "same-origin",
+        "X-XSS-Protection": "1; mode=block",
+        "X-Frame-Options": "DENY",
+        "X-Content-Type-Options": "nosniff",
+    }
+    """Headers that should be sent with each origin response."""
+
+    lambda_function_associations: list[RunwayStaticSiteLambdaFunctionAssociationDataModel] = []
+    """This allows the user to deploy custom Lambda@Edge associations with their pre-build function versions."""
+
+    namespace: str
+    """The unique namespace for the deployment."""
+
+    non_spa: bool = False
+    """Whether this site is a single page application (SPA)."""
+
+    oauth_scopes: list[str] = [
+        "phone",
+        "email",
+        "profile",
+        "openid",
+        "aws.cognito.signin.user.admin",
+    ]
+    """Scope is a mechanism in OAuth 2.0 to limit an application's access to a user's account."""
+
+    redirect_path_auth_refresh: str = "/refreshauth"
+    """The path that a user is redirected to when their authorization tokens have expired (1 hour)."""
+
+    redirect_path_sign_in: str = "/parseauth"
+    """The path that a user is redirected to after sign-in."""
+
+    redirect_path_sign_out: str = "/"
+    """The path that a user is redirected to after sign-out."""
+
+    required_group: str | None = None
+    """Name of Cognito User Pool group of which users must be a member to be granted access to the site.
+    If ``None``, allows all UserPool users to have access.
+
+    """
+
+    rewrite_directory_index: str | None = None
+    """Deploy a Lambda@Edge function designed to rewrite directory indexes."""
+
+    role_boundary_arn: str | None = None
+    """Defines an IAM Managed Policy that will be set as the permissions boundary
+    for any IAM Roles created to support the site.
+
+    """
+
+    service_role: str | None = Field(default=None, alias="cloudformation_service_role")
+    """IAM role that CloudFormation will use."""
+
+    sign_out_url: str = "/signout"
+    """The path a user should access to sign themselves out of the application."""
+
+    supported_identity_providers: list[str] = ["COGNITO"]
+    """A comma delimited list of the User Pool client identity providers."""
+
+    user_pool_arn: str | None = None
+    """The ARN of a pre-existing Cognito User Pool to use with Auth@Edge."""
+
+    web_acl: str | None = None
+    """The ARN of a web access control list (web ACL) to associate with the CloudFront Distribution."""
+
+    @field_validator(
+        "additional_redirect_domains",
+        "aliases",
+        "supported_identity_providers",
+        mode="before",
+    )
+    @classmethod
+    def _convert_comma_delimited_list(cls, v: list[str] | str) -> list[str]:
+        """Convert comma delimited lists to a string."""
+        if isinstance(v, str):
+            return [i.strip() for i in v.split(",")]
+        return v
diff --git a/runway/module/staticsite/parameters/models.py b/runway/module/staticsite/parameters/models.py
deleted file mode 100644
index 9cfd8e398..000000000
--- a/runway/module/staticsite/parameters/models.py
+++ /dev/null
@@ -1,191 +0,0 @@
-"""Runway static site Module parameters."""
-
-# ruff: noqa: UP006, UP035
-from __future__ import annotations
-
-from typing import Dict, List, Optional
-
-from pydantic import Extra, Field, validator
-
-from ....config.models.base import ConfigProperty
-
-
-class RunwayStaticSiteCustomErrorResponseDataModel(ConfigProperty):
-    """Model for Runway stat site Module staticsite_custom_error_responses parameter item.
-
-    https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-customerrorresponse.html
-
-    """
-
-    ErrorCachingMinTTL: Optional[int] = None
-    ErrorCode: Optional[int] = None
-    ResponseCode: Optional[int] = None
-    ResponsePagePath: Optional[str] = None
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module staticsite_custom_error_responses parameter item."
-
-
-class RunwayStaticSiteLambdaFunctionAssociationDataModel(ConfigProperty):
-    """Model for Runway stat site Module staticsite_lambda_function_associations parameter item.
-
-    Attributes:
-        arn: Lambda function ARN.
-        type: Association type.
-
-    """
-
-    arn: str
-    type: str
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        title = "Runway static site Module staticsite_lambda_function_associations parameter item."
-
-
-class RunwayStaticSiteModuleParametersDataModel(ConfigProperty):
-    """Model for Runway static site Module parameters.
-
-    Attributes:
-        acmcert_arn: The certificate arn used for any alias domains supplied.
-            This is a requirement when supplying any custom domain.
-        additional_redirect_domains: Additional domains (beyond the ``aliases``
-            domains or the CloudFront URL if no aliases are provided) that will
-            be authorized by the Auth@Edge UserPool AppClient.
-        aliases: Any custom domains that should be added to the CloudFront
-            Distribution.
-        auth_at_edge: Auth@Edge make the static site private by placing it behind
-            an authorization wall.
-        cf_disable: Wether deployment of the CloudFront Distribution should be
-            disabled.
-        compress: Whether the CloudFront default cache behavior will automatically
-            compress certain files.
-        cookie_settings: The default cookie settings for retrieved tokens and
-            generated nonce's.
-        create_user_pool: Wether to create a User Pool for the Auth@Edge
-            configuration.
-        custom_error_responses: Define custom error responses.
-        enable_cf_logging: Enable CloudFront logging.
-        http_headers: Headers that should be sent with each origin response.
-        lambda_function_associations: This allows the user to deploy custom
-            Lambda@Edge associations with their pre-build function versions.
-        namespace: The unique namespace for the deployment.
-        non_spa: Wether this site is a single page application (SPA).
-        oauth_scopes: Scope is a mechanism in OAuth 2.0 to limit an application's
-            access to a user's account.
-        redirect_path_auth_refresh: The path that a user is redirected to when
-            their authorization tokens have expired (1 hour).
-        redirect_path_sign_in: The path that a user is redirected to after sign-in.
-        redirect_path_sign_out: The path that a user is redirected to after sign-out.
-        required_group: Name of Cognito User Pool group of which users must be a
-            member to be granted access to the site. If ``None``, allows all
-            UserPool users to have access.
-        rewrite_directory_index: Deploy a Lambda@Edge function designed to
-            rewrite directory indexes.
-        role_boundary_arn: Defines an IAM Managed Policy that will be set as the
-            permissions boundary for any IAM Roles created to support the site.
-        service_role: IAM role that CloudFormation will use.
-        sign_out_url: The path a user should access to sign themselves out of the
-            application.
-        supported_identity_providers: A comma delimited list of the User Pool
-            client identity providers.
-        user_pool_arn: The ARN of a pre-existing Cognito User Pool to use with
-            Auth@Edge.
-        web_acl: The ARN of a web access control list (web ACL) to associate with
-            the CloudFront Distribution.
-
-    """
-
-    acmcert_arn: Optional[str] = Field(default=None, alias="staticsite_acmcert_arn")
-    additional_redirect_domains: List[str] = Field(
-        default=[], alias="staticsite_additional_redirect_domains"
-    )
-    aliases: List[str] = Field(default=[], alias="staticsite_aliases")
-    auth_at_edge: bool = Field(default=False, alias="staticsite_auth_at_edge")
-    cf_disable: bool = Field(default=False, alias="staticsite_cf_disable")
-    compress: bool = Field(default=True, alias="staticsite_compress")
-    cookie_settings: Dict[str, str] = Field(
-        default={
-            "idToken": "Path=/; Secure; SameSite=Lax",
-            "accessToken": "Path=/; Secure; SameSite=Lax",
-            "refreshToken": "Path=/; Secure; SameSite=Lax",
-            "nonce": "Path=/; Secure; HttpOnly; Max-Age=1800; SameSite=Lax",
-        },
-        alias="staticsite_cookie_settings",
-    )
-    create_user_pool: bool = Field(default=False, alias="staticsite_create_user_pool")
-    custom_error_responses: List[RunwayStaticSiteCustomErrorResponseDataModel] = Field(
-        default=[], alias="staticsite_custom_error_responses"
-    )
-    enable_cf_logging: bool = Field(default=True, alias="staticsite_enable_cf_logging")
-    http_headers: Dict[str, str] = Field(
-        default={
-            "Content-Security-Policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; "
-            "font-src 'self' 'unsafe-inline' 'unsafe-eval' data: https:; "
-            "object-src 'none'; "
-            "connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com",
-            "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
-            "Referrer-Policy": "same-origin",
-            "X-XSS-Protection": "1; mode=block",
-            "X-Frame-Options": "DENY",
-            "X-Content-Type-Options": "nosniff",
-        },
-        alias="staticsite_http_headers",
-    )
-    lambda_function_associations: List[RunwayStaticSiteLambdaFunctionAssociationDataModel] = Field(
-        default=[], alias="staticsite_lambda_function_associations"
-    )
-    namespace: str
-    non_spa: bool = Field(default=False, alias="staticsite_non_spa")
-    oauth_scopes: List[str] = Field(
-        default=[
-            "phone",
-            "email",
-            "profile",
-            "openid",
-            "aws.cognito.signin.user.admin",
-        ],
-        alias="staticsite_oauth_scopes",
-    )
-    redirect_path_auth_refresh: str = Field(
-        default="/refreshauth", alias="staticsite_redirect_path_auth_refresh"
-    )
-    redirect_path_sign_in: str = Field(
-        default="/parseauth", alias="staticsite_redirect_path_sign_in"
-    )
-    redirect_path_sign_out: str = Field(default="/", alias="staticsite_redirect_path_sign_out")
-    required_group: Optional[str] = Field(default=None, alias="staticsite_required_group")
-    rewrite_directory_index: Optional[str] = Field(
-        default=None, alias="staticsite_rewrite_directory_index"
-    )
-    role_boundary_arn: Optional[str] = Field(default=None, alias="staticsite_role_boundary_arn")
-    service_role: Optional[str] = Field(default=None, alias="cloudformation_service_role")
-    sign_out_url: str = Field(default="/signout", alias="staticsite_sign_out_url")
-    supported_identity_providers: List[str] = Field(
-        default=["COGNITO"], alias="staticsite_supported_identity_providers"
-    )
-    user_pool_arn: Optional[str] = Field(default=None, alias="staticsite_user_pool_arn")
-    web_acl: Optional[str] = Field(default=None, alias="staticsite_web_acl")
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.ignore
-        title = "Runway static site Module parameters."
-
-    @validator(  # type: ignore
-        "additional_redirect_domains",
-        "aliases",
-        "supported_identity_providers",
-        pre=True,
-    )
-    def _convert_comma_delimited_list(cls, v: list[str] | str) -> list[str]:  # noqa: N805
-        """Convert comma delimited lists to a string."""
-        if isinstance(v, str):
-            return [i.strip() for i in v.split(",")]
-        return v
diff --git a/runway/module/terraform.py b/runway/module/terraform.py
index 63a17ca17..1def347fe 100644
--- a/runway/module/terraform.py
+++ b/runway/module/terraform.py
@@ -63,6 +63,193 @@ def update_env_vars_with_tf_var_values(
     return os_env_vars
 
 
+class TerraformBackendConfig(ModuleOptions):
+    """Terraform backend configuration module options."""
+
+    def __init__(
+        self,
+        data: RunwayTerraformBackendConfigDataModel,
+        deploy_environment: DeployEnvironment,
+        path: Path,
+    ) -> None:
+        """Instantiate class.
+
+        Args:
+            data: Options parsed into a data model.
+            deploy_environment: Current deploy environment.
+            path: Module path.
+
+        """
+        self.bucket = data.bucket
+        self.data = data
+        self.dynamodb_table = data.dynamodb_table
+        self.env = deploy_environment
+        self.path = path
+        if data and not data.region:
+            data.region = deploy_environment.aws_region  # default to region from env
+        self.region = data.region
+
+    @cached_property
+    def config_file(self) -> Path | None:
+        """Backend configuration file."""
+        return self.get_backend_file(self.path, self.env.name, self.env.aws_region)
+
+    @cached_property
+    def init_args(self) -> list[str]:
+        """Return command line arguments for init."""
+        result: list[str] = []
+        for k, v in self.data.model_dump(exclude_none=True).items():
+            result.extend(["-backend-config", f"{k}={v}"])
+        if not result:
+            if self.config_file:
+                LOGGER.verbose("using backend config file: %s", self.config_file.name)
+                return [f"-backend-config={self.config_file.name}"]
+            LOGGER.info(
+                "backend file not found -- looking for one of: %s",
+                ", ".join(self.gen_backend_filenames(self.env.name, self.env.aws_region)),
+            )
+            return []
+        LOGGER.info("using backend values from runway.yml")
+        LOGGER.debug("provided backend values: %s", json.dumps(result))
+        return result
+
+    def get_full_configuration(self) -> dict[str, str]:
+        """Get full backend configuration."""
+        if not self.config_file:
+            return self.data.model_dump(exclude_none=True)
+        result = cast(dict[str, str], hcl.loads(self.config_file.read_text()))
+        result.update(self.data.model_dump(exclude_none=True))
+        return result
+
+    @classmethod
+    def get_backend_file(cls, path: Path, environment: str, region: str) -> Path | None:
+        """Determine Terraform backend file.
+
+        Args:
+            path: Path to the module.
+            environment: Current deploy environment.
+            region: Current AWS region.
+
+        """
+        backend_filenames = cls.gen_backend_filenames(environment, region)
+        for name in backend_filenames:
+            test_path = path / name
+            if test_path.is_file():
+                return test_path
+        return None
+
+    @staticmethod
+    def gen_backend_filenames(environment: str, region: str) -> list[str]:
+        """Generate possible Terraform backend filenames.
+
+        Args:
+            environment: Current deploy environment.
+            region : Current AWS region.
+
+        """
+        formats = [
+            "backend-{environment}-{region}.{extension}",
+            "backend-{environment}.{extension}",
+            "backend-{region}.{extension}",
+            "backend.{extension}",
+        ]
+        result: list[str] = []
+        for fmt in formats:
+            for ext in ["hcl", "tfvars"]:
+                result.append(  # noqa: PERF401
+                    fmt.format(environment=environment, extension=ext, region=region)
+                )
+        return result
+
+    @classmethod
+    def parse_obj(
+        cls,
+        deploy_environment: DeployEnvironment,
+        obj: object,
+        path: Path | None = None,
+    ) -> TerraformBackendConfig:
+        """Parse options definition and return an options object.
+
+        Args:
+            deploy_environment: Current deploy environment.
+            obj: Object to parse.
+            path: Module path.
+
+        """
+        return cls(
+            data=RunwayTerraformBackendConfigDataModel.model_validate(obj),
+            deploy_environment=deploy_environment,
+            path=path or Path.cwd(),
+        )
+
+
+class TerraformOptions(ModuleOptions):
+    """Module options for Terraform.
+
+    Attributes:
+        args: CLI arguments/options to pass to Terraform.
+        data: Options parsed into a data model.
+        env: Current deploy environment.
+        path: Module path.
+        version: String containing a Terraform version.
+        write_auto_tfvars: Optionally write parameters to a tfvars file instead
+            of updating variables.
+
+    """
+
+    def __init__(
+        self,
+        data: RunwayTerraformModuleOptionsDataModel,
+        deploy_environment: DeployEnvironment,
+        path: Path | None = None,
+    ) -> None:
+        """Instantiate class.
+
+        Args:
+            deploy_environment: Current deploy environment.
+            data: Options parsed into a data model.
+            path: Module path.
+
+        """
+        self.args = data.args
+        self.data = data
+        self.env = deploy_environment
+        self.path = path or Path.cwd()
+        self.version = data.version
+        self.workspace = data.workspace or deploy_environment.name
+        self.write_auto_tfvars = data.write_auto_tfvars
+
+    @cached_property
+    def backend_config(self) -> TerraformBackendConfig:
+        """Backend configuration options."""
+        return TerraformBackendConfig.parse_obj(
+            deploy_environment=self.env,
+            obj=self.data.backend_config or {},
+            path=self.path,
+        )
+
+    @classmethod
+    def parse_obj(
+        cls,
+        deploy_environment: DeployEnvironment,
+        obj: object,
+        path: Path | None = None,
+    ) -> TerraformOptions:
+        """Parse options definition and return an options object.
+
+        Args:
+            deploy_environment: Current deploy environment.
+            obj: Object to parse.
+            path: Module path.
+
+        """
+        return cls(
+            data=RunwayTerraformModuleOptionsDataModel.model_validate(obj),
+            deploy_environment=deploy_environment,
+            path=path or Path.cwd(),
+        )
+
+
 TerraformActionTypeDef = Literal[
     "apply",
     "destroy",
@@ -76,11 +263,9 @@ def update_env_vars_with_tf_var_values(
 ]
 
 
-class Terraform(RunwayModule, DelCachedPropMixin):
+class Terraform(RunwayModule[TerraformOptions], DelCachedPropMixin):
     """Terraform Runway Module."""
 
-    options: TerraformOptions
-
     def __init__(
         self,
         context: RunwayContext,
@@ -240,9 +425,7 @@ def destroy(self) -> None:
         self.run("destroy")
 
     def gen_command(
-        self,
-        command: list[str] | str | tuple[str, ...],
-        args_list: list[str] | None = None,
+        self, command: list[str] | str | tuple[str, ...], args_list: list[str] | None = None
     ) -> list[str]:
         """Generate Terraform command."""
         cmd = (
@@ -546,190 +729,3 @@ def run(self, action: TerraformActionTypeDef) -> None:
         finally:
             if self.auto_tfvars.exists():
                 self.auto_tfvars.unlink()
-
-
-class TerraformOptions(ModuleOptions):
-    """Module options for Terraform.
-
-    Attributes:
-        args: CLI arguments/options to pass to Terraform.
-        data: Options parsed into a data model.
-        env: Current deploy environment.
-        path: Module path.
-        version: String containing a Terraform version.
-        write_auto_tfvars: Optionally write parameters to a tfvars file instead
-            of updating variables.
-
-    """
-
-    def __init__(
-        self,
-        data: RunwayTerraformModuleOptionsDataModel,
-        deploy_environment: DeployEnvironment,
-        path: Path | None = None,
-    ) -> None:
-        """Instantiate class.
-
-        Args:
-            deploy_environment: Current deploy environment.
-            data: Options parsed into a data model.
-            path: Module path.
-
-        """
-        self.args = data.args
-        self.data = data
-        self.env = deploy_environment
-        self.path = path or Path.cwd()
-        self.version = data.version
-        self.workspace = data.workspace or deploy_environment.name
-        self.write_auto_tfvars = data.write_auto_tfvars
-
-    @cached_property
-    def backend_config(self) -> TerraformBackendConfig:
-        """Backend configuration options."""
-        return TerraformBackendConfig.parse_obj(
-            deploy_environment=self.env,
-            obj=self.data.backend_config or {},
-            path=self.path,
-        )
-
-    @classmethod
-    def parse_obj(
-        cls,
-        deploy_environment: DeployEnvironment,
-        obj: object,
-        path: Path | None = None,
-    ) -> TerraformOptions:
-        """Parse options definition and return an options object.
-
-        Args:
-            deploy_environment: Current deploy environment.
-            obj: Object to parse.
-            path: Module path.
-
-        """
-        return cls(
-            data=RunwayTerraformModuleOptionsDataModel.parse_obj(obj),
-            deploy_environment=deploy_environment,
-            path=path or Path.cwd(),
-        )
-
-
-class TerraformBackendConfig(ModuleOptions):
-    """Terraform backend configuration module options."""
-
-    def __init__(
-        self,
-        data: RunwayTerraformBackendConfigDataModel,
-        deploy_environment: DeployEnvironment,
-        path: Path,
-    ) -> None:
-        """Instantiate class.
-
-        Args:
-            data: Options parsed into a data model.
-            deploy_environment: Current deploy environment.
-            path: Module path.
-
-        """
-        self.bucket = data.bucket
-        self.data = data
-        self.dynamodb_table = data.dynamodb_table
-        self.env = deploy_environment
-        self.path = path
-        if data and not data.region:
-            data.region = deploy_environment.aws_region  # default to region from env
-        self.region = data.region
-
-    @cached_property
-    def config_file(self) -> Path | None:
-        """Backend configuration file."""
-        return self.get_backend_file(self.path, self.env.name, self.env.aws_region)
-
-    @cached_property
-    def init_args(self) -> list[str]:
-        """Return command line arguments for init."""
-        result: list[str] = []
-        for k, v in self.data.dict(exclude_none=True).items():
-            result.extend(["-backend-config", f"{k}={v}"])
-        if not result:
-            if self.config_file:
-                LOGGER.verbose("using backend config file: %s", self.config_file.name)
-                return [f"-backend-config={self.config_file.name}"]
-            LOGGER.info(
-                "backend file not found -- looking for one of: %s",
-                ", ".join(self.gen_backend_filenames(self.env.name, self.env.aws_region)),
-            )
-            return []
-        LOGGER.info("using backend values from runway.yml")
-        LOGGER.debug("provided backend values: %s", json.dumps(result))
-        return result
-
-    def get_full_configuration(self) -> dict[str, str]:
-        """Get full backend configuration."""
-        if not self.config_file:
-            return self.data.dict(exclude_none=True)
-        result = cast(dict[str, str], hcl.loads(self.config_file.read_text()))
-        result.update(self.data.dict(exclude_none=True))
-        return result
-
-    @classmethod
-    def get_backend_file(cls, path: Path, environment: str, region: str) -> Path | None:
-        """Determine Terraform backend file.
-
-        Args:
-            path: Path to the module.
-            environment: Current deploy environment.
-            region: Current AWS region.
-
-        """
-        backend_filenames = cls.gen_backend_filenames(environment, region)
-        for name in backend_filenames:
-            test_path = path / name
-            if test_path.is_file():
-                return test_path
-        return None
-
-    @staticmethod
-    def gen_backend_filenames(environment: str, region: str) -> list[str]:
-        """Generate possible Terraform backend filenames.
-
-        Args:
-            environment: Current deploy environment.
-            region : Current AWS region.
-
-        """
-        formats = [
-            "backend-{environment}-{region}.{extension}",
-            "backend-{environment}.{extension}",
-            "backend-{region}.{extension}",
-            "backend.{extension}",
-        ]
-        result: list[str] = []
-        for fmt in formats:
-            for ext in ["hcl", "tfvars"]:
-                result.append(  # noqa: PERF401
-                    fmt.format(environment=environment, extension=ext, region=region)
-                )
-        return result
-
-    @classmethod
-    def parse_obj(
-        cls,
-        deploy_environment: DeployEnvironment,
-        obj: object,
-        path: Path | None = None,
-    ) -> TerraformBackendConfig:
-        """Parse options definition and return an options object.
-
-        Args:
-            deploy_environment: Current deploy environment.
-            obj: Object to parse.
-            path: Module path.
-
-        """
-        return cls(
-            data=RunwayTerraformBackendConfigDataModel.parse_obj(obj),
-            deploy_environment=deploy_environment,
-            path=path or Path.cwd(),
-        )
diff --git a/runway/s3_utils.py b/runway/s3_utils.py
index 36f36b8fa..ef84b1998 100644
--- a/runway/s3_utils.py
+++ b/runway/s3_utils.py
@@ -17,7 +17,10 @@
 
     from mypy_boto3_s3.client import S3Client
     from mypy_boto3_s3.service_resource import S3ServiceResource
-    from mypy_boto3_s3.type_defs import ObjectTypeDef
+    from mypy_boto3_s3.type_defs import (
+        ListObjectsV2RequestListObjectsV2PaginateTypeDef,
+        ObjectTypeDef,
+    )
 
     from ._logging import RunwayLogger
 
@@ -188,7 +191,7 @@ def get_matching_s3_objects(
     s3_client = _get_client(session)
     paginator = s3_client.get_paginator("list_objects_v2")
 
-    kwargs = {"Bucket": bucket}
+    kwargs: ListObjectsV2RequestListObjectsV2PaginateTypeDef = {"Bucket": bucket}
 
     # We can pass the prefix directly to the S3 API.  If the user has passed
     # a tuple or list of prefixes, we go through them one by one.
@@ -197,12 +200,7 @@ def get_matching_s3_objects(
         kwargs["Prefix"] = key_prefix
 
         for page in paginator.paginate(**kwargs):
-            try:
-                contents = page["Contents"]
-            except KeyError:
-                return
-
-            for obj in contents:
+            for obj in page.get("Contents", []):
                 if "Key" in obj and obj["Key"].endswith(suffix):
                     yield obj
 
diff --git a/runway/templates/k8s-cfn-repo/k8s-workers.cfn/local_lookups/bootstrap_value.py b/runway/templates/k8s-cfn-repo/k8s-workers.cfn/local_lookups/bootstrap_value.py
index 9b690c72e..41a70884d 100644
--- a/runway/templates/k8s-cfn-repo/k8s-workers.cfn/local_lookups/bootstrap_value.py
+++ b/runway/templates/k8s-cfn-repo/k8s-workers.cfn/local_lookups/bootstrap_value.py
@@ -41,7 +41,7 @@ class BootstrapValue(LookupHandler):
     """
 
     @classmethod
-    def handle(  # type: ignore
+    def handle(
         cls,
         value: str,
         context: CfnginContext,
@@ -51,7 +51,7 @@ def handle(  # type: ignore
     ) -> str:
         """Handle lookup."""
         query, raw_args = cls.parse(value)
-        args = HookArgs.parse_obj(raw_args)
+        args = HookArgs.model_validate(raw_args)
 
         stack = context.get_stack(query)
         if not stack:
diff --git a/runway/type_defs.py b/runway/type_defs.py
index 6f58fd24c..504d40cfa 100644
--- a/runway/type_defs.py
+++ b/runway/type_defs.py
@@ -3,11 +3,14 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TypeVar, Union
+from typing import TYPE_CHECKING, TypeVar
 
 from typing_extensions import TypedDict
 
-AnyPath = Union[Path, str]
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+AnyPath: TypeAlias = "Path | str"
 AnyPathConstrained = TypeVar("AnyPathConstrained", Path, str)
 
 
diff --git a/runway/utils/__init__.py b/runway/utils/__init__.py
index dc202d92f..130269274 100644
--- a/runway/utils/__init__.py
+++ b/runway/utils/__init__.py
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-import datetime
 import hashlib
 import importlib
 import json
@@ -14,7 +13,6 @@
 import sys
 from collections.abc import Iterable, Iterator, MutableMapping
 from contextlib import AbstractContextManager, contextmanager
-from decimal import Decimal
 from functools import cached_property  # noqa: F401  # TODO (kyle): remove in next major release
 from pathlib import Path
 from subprocess import check_call
@@ -25,7 +23,9 @@
 
 # make this importable without defining __all__ yet.
 # more things need to be moved of this file before starting an explicit __all__.
+from . import pydantic_validators  # noqa: F401
 from ._file_hash import FileHash  # noqa: F401
+from ._json_encoder import JsonEncoder  # noqa: F401
 from ._version import Version  # noqa: F401
 
 if TYPE_CHECKING:
@@ -63,7 +63,10 @@ def __contains__(self, name: object) -> bool:
             name: The name to check for existence in the model.
 
         """
-        return name in self.__dict__
+        if name in self.__dict__:
+            return True
+        # extra files are no longer added to __dict__, they are placed in `model_extra`
+        return bool(self.model_extra and name in self.model_extra)
 
     def __getitem__(self, name: str) -> Any:
         """Implement evaluation of self[name].
@@ -88,40 +91,7 @@ def __setitem__(self, name: str, value: Any) -> None:
             value: Value to assign to the attribute.
 
         """
-        super().__setattr__(name, value)  # type: ignore
-
-
-class JsonEncoder(json.JSONEncoder):
-    """Encode Python objects to JSON data.
-
-    This class can be used with ``json.dumps()`` to handle most data types
-    that can occur in responses from AWS.
-
-    Usage:
-        >>> json.dumps(data, cls=JsonEncoder)
-
-    """
-
-    def default(self, o: Any) -> Any:
-        """Encode types not supported by the default JSONEncoder.
-
-        Args:
-            o: Object to encode.
-
-        Returns:
-            JSON serializable data type.
-
-        Raises:
-            TypeError: Object type could not be encoded.
-
-        """
-        if isinstance(o, Decimal):
-            return float(o)
-        if isinstance(o, (datetime.datetime, datetime.date)):
-            return o.isoformat()
-        if isinstance(o, _BaseModel):
-            return o.dict()
-        return super().default(o)
+        super().__setattr__(name, value)
 
 
 class MutableMap(MutableMapping[str, Any]):
@@ -455,7 +425,7 @@ class YamlDumper(yaml.Dumper):
 
     def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:  # noqa: ARG002
         """Override parent method."""
-        return super().increase_indent(flow, False)  # type: ignore
+        return super().increase_indent(flow, False)
 
 
 @contextmanager
diff --git a/runway/utils/_file_hash.py b/runway/utils/_file_hash.py
index 33448211a..2f8758c81 100644
--- a/runway/utils/_file_hash.py
+++ b/runway/utils/_file_hash.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TYPE_CHECKING, ClassVar, Optional
+from typing import TYPE_CHECKING, ClassVar
 
 if TYPE_CHECKING:
     import hashlib
@@ -84,7 +84,7 @@ def add_file_name(
         file_path: StrPath,
         *,
         end_character: str = "\0",
-        relative_to: Optional[StrPath] = None,
+        relative_to: StrPath | None = None,
     ) -> None:
         """Add file name to the hash. This includes the path.
 
@@ -109,7 +109,7 @@ def add_files(
         self,
         file_paths: Iterable[StrPath],
         *,
-        relative_to: Optional[StrPath] = None,
+        relative_to: StrPath | None = None,
     ) -> None:
         """Add files to the hash.
 
diff --git a/runway/utils/_json_encoder.py b/runway/utils/_json_encoder.py
new file mode 100644
index 000000000..28b4ca1f6
--- /dev/null
+++ b/runway/utils/_json_encoder.py
@@ -0,0 +1,49 @@
+"""Customized JSON encoder."""
+
+from __future__ import annotations
+
+import datetime
+import json
+from decimal import Decimal
+from pathlib import Path
+from typing import Any
+
+from packaging.specifiers import SpecifierSet
+from pydantic import BaseModel
+
+
+class JsonEncoder(json.JSONEncoder):
+    """Encode Python objects to JSON data.
+
+    This class can be used with ``json.dumps()`` to handle most data types
+    that can occur in responses from AWS.
+
+    Usage:
+        >>> json.dumps(data, cls=JsonEncoder)
+
+    """
+
+    def default(self, o: Any) -> dict[Any, Any] | float | list[Any] | str | Any:
+        """Encode types not supported by the default JSONEncoder.
+
+        Args:
+            o: Object to encode.
+
+        Returns:
+            JSON serializable data type.
+
+        Raises:
+            TypeError: Object type could not be encoded.
+
+        """
+        if isinstance(o, Decimal):
+            return float(o)
+        if isinstance(o, (datetime.datetime, datetime.date)):
+            return o.isoformat()
+        if isinstance(o, BaseModel):
+            return o.model_dump()
+        if isinstance(o, (Path, SpecifierSet)):
+            return str(o)
+        if isinstance(o, (set, tuple)):
+            return list(o)  # pyright: ignore[reportUnknownArgumentType]
+        return super().default(o)
diff --git a/runway/utils/pydantic_validators/__init__.py b/runway/utils/pydantic_validators/__init__.py
new file mode 100644
index 000000000..e39d7763e
--- /dev/null
+++ b/runway/utils/pydantic_validators/__init__.py
@@ -0,0 +1,5 @@
+"""Pydantic validators."""
+
+from ._lax_str import LaxStr
+
+__all__ = ["LaxStr"]
diff --git a/runway/utils/pydantic_validators/_lax_str.py b/runway/utils/pydantic_validators/_lax_str.py
new file mode 100644
index 000000000..c986e8bf6
--- /dev/null
+++ b/runway/utils/pydantic_validators/_lax_str.py
@@ -0,0 +1,36 @@
+"""Inverse of :class:`~pydantic.types.StrictStr`."""
+
+from __future__ import annotations
+
+from decimal import Decimal
+
+from pydantic.functional_validators import BeforeValidator
+
+
+def _handler(value: object | None) -> object | None:
+    """Convert the provided value if able."""
+    if isinstance(value, (float, int, Decimal)):
+        return str(value)
+    return value
+
+
+LaxStr = BeforeValidator(_handler)
+"""Custom :class:`~pydantic.functional_validators.BeforeValidator`.
+
+Inverse of :class:`~pydantic.types.StrictStr` that allows additional types to be
+accepted as a :class:`str`.
+
+.. rubric:: Example
+.. code-block:: python
+
+    from __future__ import annotations
+
+    from typing import Annotated
+
+    from runway.utils import LaxStr
+
+    class MyModel(BaseModel):
+        some_field: Annotated[str, LaxStr]
+        some_other_field: Annotated[str | None, LaxStr] = None
+
+"""
diff --git a/runway/variables.py b/runway/variables.py
index 23fb4d000..e65142810 100644
--- a/runway/variables.py
+++ b/runway/variables.py
@@ -263,18 +263,18 @@ def parse_obj(  # noqa: C901
 
         """
         if isinstance(obj, BaseModel):
-            return VariableValuePydanticModel(obj, variable_type=variable_type)  # type: ignore
+            return VariableValuePydanticModel(obj, variable_type=variable_type)
         if isinstance(obj, dict):
             return VariableValueDict(obj, variable_type=variable_type)  # type: ignore
         if isinstance(obj, list):
             return VariableValueList(obj, variable_type=variable_type)  # type: ignore
         if not isinstance(obj, str):
-            return VariableValueLiteral(obj, variable_type=variable_type)  # type: ignore
+            return VariableValueLiteral(obj, variable_type=variable_type)
 
         tokens: VariableValueConcatenation[VariableValueLiteral[str] | VariableValueLookup] = (
             VariableValueConcatenation(
                 # pyright 1.1.138 is having issues properly inferring the type from comprehension
-                [  # type: ignore
+                [
                     VariableValueLiteral(cast(str, t), variable_type=variable_type)
                     for t in re.split(r"(\$\{|\}|\s+)", obj)  # ${ or space or }
                 ]
@@ -516,7 +516,7 @@ def __getitem__(  # pyright: ignore[reportIncompatibleMethodOverride]
         self, __index: int | slice
     ) -> MutableSequence[VariableValue] | VariableValue:
         """Get item by index."""
-        return self._data[__index]  # pyright: ignore[reportCallIssue]
+        return self._data[__index]
 
     @overload
     def __setitem__(self, __index: int, __value: VariableValue) -> None: ...
@@ -639,7 +639,7 @@ def simplified(self) -> VariableValue:
                 concat[-1] = VariableValueLiteral(
                     str(concat[-1].value) + str(item.value)  # type: ignore
                 )
-            elif isinstance(item, VariableValueConcatenation):  # type: ignore
+            elif isinstance(item, VariableValueConcatenation):
                 concat.extend(iter(item.simplified))
             else:
                 concat.append(item.simplified)
@@ -917,7 +917,7 @@ def value(self) -> _PydanticModelTypeVar:
         into a pydantic model.
 
         """
-        return self._model_class.parse_obj(
+        return self._model_class.model_validate(
             {field: value.value for field, value in self._data.items()}
         )
 
diff --git a/typings/awacs/__init__.pyi b/typings/awacs/__init__.pyi
index 40cd9c366..9f6b488b4 100644
--- a/typings/awacs/__init__.pyi
+++ b/typings/awacs/__init__.pyi
@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import json
 import re
-from typing import Any, Dict, Optional, Union
+from typing import Any
 
 __version__ = "1.0.1"
 valid_names = re.compile(r"^[a-zA-Z0-9]+$")
@@ -14,15 +14,15 @@ class AWSObject(object):
         self,
         name: str,
         type: Any = ...,
-        dictname: Optional[str] = ...,
-        props: Dict[str, Any] = ...,
+        dictname: str | None = ...,
+        props: dict[str, Any] = ...,
         **kwargs: Any,
     ) -> None: ...
     def __getattr__(self, name: str) -> Any: ...
     def __setattr__(self, name: str, value: Any) -> None: ...
     def validate(self) -> None: ...
-    def JSONrepr(self) -> Dict[str, Any]: ...
-    def to_json(self, indent: Optional[int] = ..., sort_keys: bool = ...) -> str: ...
+    def JSONrepr(self) -> dict[str, Any]: ...
+    def to_json(self, indent: int | None = ..., sort_keys: bool = ...) -> str: ...
     def __eq__(self, other: Any) -> bool: ...
     def __ne__(self, other: Any) -> bool: ...
     def __hash__(self) -> int: ...
@@ -37,8 +37,8 @@ class AWSProperty(AWSObject):
     def __init__(self, **kwargs: Any) -> None: ...
 
 class AWSHelperFn(object):
-    def getdata(self, data: object) -> Union[str, object]: ...
-    def to_json(self, indent: Optional[int] = ..., sort_keys: bool = ...) -> str: ...
+    def getdata(self, data: object) -> str | object: ...
+    def to_json(self, indent: int | None = ..., sort_keys: bool = ...) -> str: ...
     def __eq__(self, other: Any) -> bool: ...
     def __ne__(self, other: Any) -> bool: ...
     def __hash__(self) -> int: ...
diff --git a/typings/awacs/aws.pyi b/typings/awacs/aws.pyi
index 059f5223b..2b6421e08 100644
--- a/typings/awacs/aws.pyi
+++ b/typings/awacs/aws.pyi
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Dict, List, Union
+from typing import TYPE_CHECKING, Any
 
 from . import AWSHelperFn, AWSProperty
 
@@ -48,7 +48,7 @@ class ARN(BaseARN):
 class ConditionElement(AWSHelperFn):
     def __init__(
         self,
-        data: Union[Dict[str, Any], str],
+        data: dict[str, Any] | str,
         value: Any = ...,
     ) -> None:
         """Create a ConditionElement
@@ -68,7 +68,7 @@ class ConditionElement(AWSHelperFn):
         """
         ...
 
-    def get_dict(self) -> Dict[str, Union[List[str], str]]: ...
+    def get_dict(self) -> dict[str, list[str] | str]: ...
 
 class Condition(AWSHelperFn):
     def __init__(self, conditions) -> None: ...
@@ -79,26 +79,26 @@ class Principal(AWSHelperFn):
     def __init__(
         self,
         principal: str,
-        resources: Union[
-            List[str],
-            List[troposphere.AWSHelperFn],
-            List[Union[str, troposphere.AWSHelperFn]],
-            str,
-            troposphere.AWSHelperFn,
-        ] = ...,
+        resources: (
+            list[str]
+            | list[troposphere.AWSHelperFn]
+            | list[str | troposphere.AWSHelperFn]
+            | str
+            | troposphere.AWSHelperFn
+        ) = ...,
     ) -> None: ...
     def JSONrepr(self): ...
 
 class AWSPrincipal(Principal):
     def __init__(
         self,
-        principals: Union[
-            List[str],
-            List[troposphere.AWSHelperFn],
-            List[Union[str, troposphere.AWSHelperFn]],
-            str,
-            troposphere.AWSHelperFn,
-        ],
+        principals: (
+            list[str]
+            | list[troposphere.AWSHelperFn]
+            | list[str | troposphere.AWSHelperFn]
+            | str
+            | troposphere.AWSHelperFn
+        ),
     ) -> None: ...
 
 def effect(x): ...
diff --git a/typings/botocore/credentials.pyi b/typings/botocore/credentials.pyi
index de61b01ef..803f1238c 100644
--- a/typings/botocore/credentials.pyi
+++ b/typings/botocore/credentials.pyi
@@ -3,15 +3,14 @@ This type stub file was generated by pyright.
 """
 
 import logging
-from collections import namedtuple
-from typing import NamedTuple, Optional
+from typing import NamedTuple
 
 logger = logging.getLogger(__name__)
 
 class ReadOnlyCredentials(NamedTuple):
     access_key: str
     secret_key: str
-    token: Optional[str]
+    token: str | None
 
 def create_credential_resolver(session, cache=..., region_name=...):
     """Create a default credential resolver.
diff --git a/typings/botocore/response.pyi b/typings/botocore/response.pyi
index cb8d3d14b..293090853 100644
--- a/typings/botocore/response.pyi
+++ b/typings/botocore/response.pyi
@@ -1,7 +1,7 @@
 """This type stub file was generated by pyright."""
 
 import logging
-from typing import BinaryIO, Generator, Optional
+from typing import BinaryIO, Generator
 
 logger = logging.getLogger(__name__)
 
@@ -27,7 +27,7 @@ class StreamingBody(object):
         """Set the timeout seconds on the socket."""
         ...
 
-    def read(self, amt: Optional[int] = ...) -> bytes:
+    def read(self, amt: int | None = ...) -> bytes:
         """Read at most amt bytes from the stream.
 
         If the amt argument is omitted, read all data.
diff --git a/typings/docker/api/image.pyi b/typings/docker/api/image.pyi
index 57c6329fc..3b56a82aa 100644
--- a/typings/docker/api/image.pyi
+++ b/typings/docker/api/image.pyi
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import Any, Dict, Iterator, List, Optional, Union
+from typing import Any, Iterator
 
 from docker import utils
 
@@ -19,8 +19,8 @@ class ImageApiMixin:
         name: str = ...,
         quiet: bool = ...,
         all: bool = ...,
-        filters: Optional[Dict[str, Any]] = ...,
-    ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: ...
+        filters: dict[str, Any] | None = ...,
+    ) -> dict[str, Any] | list[dict[str, Any]]: ...
     def import_image(
         self, src=..., repository=..., tag=..., image=..., changes=..., stream_src=...
     ): ...
@@ -30,41 +30,41 @@ class ImageApiMixin:
     def import_image_from_url(self, url, repository=..., tag=..., changes=...): ...
     def import_image_from_image(self, image, repository=..., tag=..., changes=...): ...
     @utils.check_resource("image")
-    def inspect_image(self, image: str) -> Dict[str, Any]: ...
+    def inspect_image(self, image: str) -> dict[str, Any]: ...
     @utils.minimum_version("1.30")
     @utils.check_resource("image")
     def inspect_distribution(
-        self, image: str, auth_config: Optional[Dict[str, Any]] = ...
-    ) -> Dict[str, Any]: ...
-    def load_image(self, data: bytes, quiet: bool = ...) -> Iterator[Dict[str, Any]]: ...
+        self, image: str, auth_config: dict[str, Any] | None = ...
+    ) -> dict[str, Any]: ...
+    def load_image(self, data: bytes, quiet: bool = ...) -> Iterator[dict[str, Any]]: ...
     @utils.minimum_version("1.25")
-    def prune_images(self, filters: Optional[Dict[str, Any]] = ...) -> Dict[str, Any]: ...
+    def prune_images(self, filters: dict[str, Any] | None = ...) -> dict[str, Any]: ...
     def pull(
         self,
-        repository: Optional[str],
-        tag: Optional[str] = ...,
+        repository: str | None,
+        tag: str | None = ...,
         stream: bool = ...,
-        auth_config: Optional[Dict[str, Any]] = ...,
+        auth_config: dict[str, Any] | None = ...,
         decode: bool = ...,
-        platform: Optional[str] = ...,
+        platform: str | None = ...,
         all_tags: bool = ...,
-    ) -> Union[Iterator[Dict[str, Any]], Iterator[bytes], Iterator[str], str]: ...
+    ) -> Iterator[dict[str, Any]] | Iterator[bytes] | Iterator[str] | str: ...
     def push(
         self,
-        repository: Optional[str],
-        tag: Optional[str] = ...,
+        repository: str | None,
+        tag: str | None = ...,
         stream: bool = ...,
-        auth_config: Optional[Dict[str, Any]] = ...,
+        auth_config: dict[str, Any] | None = ...,
         decode: bool = ...,
-    ) -> Union[Iterator[Dict[str, Any]], Iterator[bytes], Iterator[str], str]: ...
+    ) -> Iterator[dict[str, Any]] | Iterator[bytes] | Iterator[str] | str: ...
     @utils.check_resource("image")
     def remove_image(
         self, image: str, force: bool = ..., noprune: bool = ...
-    ) -> Dict[str, Any]: ...
-    def search(self, term: str, limit: Optional[int] = ...) -> List[Dict[str, Any]]: ...
+    ) -> dict[str, Any]: ...
+    def search(self, term: str, limit: int | None = ...) -> list[dict[str, Any]]: ...
     @utils.check_resource("image")
     def tag(
-        self, image: str, repository: Optional[str], tag: str = ..., force: bool = ...
+        self, image: str, repository: str | None, tag: str = ..., force: bool = ...
     ) -> bool: ...
 
 def is_file(src: str) -> bool: ...
diff --git a/typings/s3transfer/futures.pyi b/typings/s3transfer/futures.pyi
index d6c2c2e9a..55e287a04 100644
--- a/typings/s3transfer/futures.pyi
+++ b/typings/s3transfer/futures.pyi
@@ -3,19 +3,7 @@
 from __future__ import annotations
 
 from concurrent import futures
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    ClassVar,
-    Dict,
-    List,
-    NamedTuple,
-    Optional,
-    Set,
-    Type,
-    Union,
-)
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, NamedTuple
 
 from typing_extensions import Literal
 
@@ -42,15 +30,15 @@ class BaseTransferMeta:
     @property
     def call_args(self) -> CallArgs: ...
     @property
-    def transfer_id(self) -> Optional[str]: ...
+    def transfer_id(self) -> str | None: ...
     @property
-    def user_context(self) -> Dict[Any, Any]: ...
+    def user_context(self) -> dict[Any, Any]: ...
 
 class TransferFuture(BaseTransferFuture):
     def __init__(
         self,
-        meta: Optional[TransferMeta] = ...,
-        coordinator: Optional[TransferCoordinator] = ...,
+        meta: TransferMeta | None = ...,
+        coordinator: TransferCoordinator | None = ...,
     ) -> None: ...
     @property
     def meta(self) -> TransferMeta: ...
@@ -62,30 +50,28 @@ class TransferFuture(BaseTransferFuture):
 class TransferMeta(BaseTransferMeta):
     """Holds metadata about the TransferFuture"""
 
-    def __init__(
-        self, call_args: Optional[CallArgs] = ..., transfer_id: Optional[str] = ...
-    ) -> None: ...
+    def __init__(self, call_args: CallArgs | None = ..., transfer_id: str | None = ...) -> None: ...
     @property
     def call_args(self) -> CallArgs: ...
     @property
-    def transfer_id(self) -> Optional[str]: ...
+    def transfer_id(self) -> str | None: ...
     @property
-    def size(self) -> Optional[int]: ...
+    def size(self) -> int | None: ...
     @property
-    def user_context(self) -> Dict[Any, Any]: ...
+    def user_context(self) -> dict[Any, Any]: ...
     def provide_transfer_size(self, size: int) -> None: ...
 
 class TransferCoordinator:
     """A helper class for managing TransferFuture"""
 
-    def __init__(self, transfer_id: Optional[str] = ...) -> None: ...
+    def __init__(self, transfer_id: str | None = ...) -> None: ...
     def __repr__(self) -> str: ...
     @property
-    def exception(self) -> Optional[Exception]: ...
+    def exception(self) -> Exception | None: ...
     @property
-    def associated_futures(self) -> Set[futures.Future[Any]]: ...
+    def associated_futures(self) -> set[futures.Future[Any]]: ...
     @property
-    def failure_cleanups(self) -> List[Callable[..., Any]]: ...
+    def failure_cleanups(self) -> list[Callable[..., Any]]: ...
     @property
     def status(
         self,
@@ -97,7 +83,7 @@ class TransferCoordinator:
     def set_status_to_queued(self) -> None: ...
     def set_status_to_running(self) -> None: ...
     def submit(
-        self, executor: BoundedExecutor, task: Task, tag: Optional[TaskTag] = ...
+        self, executor: BoundedExecutor, task: Task, tag: TaskTag | None = ...
     ) -> futures.Future: ...
     def done(self) -> bool: ...
     def add_associated_future(self, future: futures.Future[Any]) -> None: ...
@@ -116,11 +102,11 @@ class BoundedExecutor:
         self,
         max_size: int,
         max_num_threads: int,
-        tag_semaphores: Dict[str, Any] = ...,
-        executor_cls: Type[Union[BaseExecutor, futures.ThreadPoolExecutor]] = ...,
+        tag_semaphores: dict[str, Any] = ...,
+        executor_cls: type[BaseExecutor | futures.ThreadPoolExecutor] = ...,
     ) -> None: ...
     def submit(
-        self, task: Task, tag: Optional[TaskTag] = ..., block: bool = ...
+        self, task: Task, tag: TaskTag | None = ..., block: bool = ...
     ) -> ExecutorFuture: ...
     def shutdown(self, wait: bool = ...) -> None: ...
 
@@ -131,7 +117,7 @@ class ExecutorFuture:
     def done(self) -> bool: ...
 
 class BaseExecutor:
-    def __init__(self, max_workers: Optional[int] = ...) -> None: ...
+    def __init__(self, max_workers: int | None = ...) -> None: ...
     def submit(
         self, fn: Callable[..., Any], *args: Any, **kwargs: Any
     ) -> NonThreadedExecutorFuture: ...
@@ -147,7 +133,7 @@ class NonThreadedExecutorFuture:
     def __init__(self) -> None: ...
     def set_result(self, result: Any) -> None: ...
     def set_exception_info(self, exception: Exception, traceback: TracebackException) -> None: ...
-    def result(self, timeout: Optional[int] = ...) -> Any: ...
+    def result(self, timeout: int | None = ...) -> Any: ...
     def done(self) -> bool: ...
     def add_done_callback(self, fn: Callable[..., Any]) -> None: ...
 
diff --git a/typings/s3transfer/manager.pyi b/typings/s3transfer/manager.pyi
index 9f8b94802..cbc39f51e 100644
--- a/typings/s3transfer/manager.pyi
+++ b/typings/s3transfer/manager.pyi
@@ -2,20 +2,7 @@
 
 from __future__ import annotations
 
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    ClassVar,
-    Dict,
-    Generic,
-    List,
-    Optional,
-    Set,
-    TextIO,
-    Type,
-    TypeVar,
-    Union,
-)
+from typing import TYPE_CHECKING, Any, ClassVar, Generic, Optional, TextIO, TypeVar
 
 from .futures import BaseExecutor, TransferFuture
 from .subscribers import BaseSubscriber
@@ -59,18 +46,18 @@ class TransferConfig:
     ) -> None: ...
 
 class TransferManager:
-    ALLOWED_DOWNLOAD_ARGS: ClassVar[List[str]] = ...
-    ALLOWED_UPLOAD_ARGS: ClassVar[List[str]] = ...
-    ALLOWED_COPY_ARGS: ClassVar[List[str]] = ...
-    ALLOWED_DELETE_ARGS: ClassVar[List[str]] = ...
+    ALLOWED_DOWNLOAD_ARGS: ClassVar[list[str]] = ...
+    ALLOWED_UPLOAD_ARGS: ClassVar[list[str]] = ...
+    ALLOWED_COPY_ARGS: ClassVar[list[str]] = ...
+    ALLOWED_DELETE_ARGS: ClassVar[list[str]] = ...
     VALIDATE_SUPPORTED_BUCKET_VALUES: ClassVar[bool] = ...
-    _UNSUPPORTED_BUCKET_PATTERNS: ClassVar[Dict[str, Pattern[str]]] = ...
+    _UNSUPPORTED_BUCKET_PATTERNS: ClassVar[dict[str, Pattern[str]]] = ...
     def __init__(
         self,
         client: S3Client,
         config: Optional[TransferConfig] = ...,
         osutil: Optional[OSUtils] = ...,
-        executor_cls: Optional[Type[BaseExecutor]] = ...,
+        executor_cls: Optional[type[BaseExecutor]] = ...,
     ) -> None: ...
     @property
     def client(self) -> S3Client: ...
@@ -78,40 +65,40 @@ class TransferManager:
     def config(self) -> TransferConfig: ...
     def upload(
         self,
-        fileobj: Union[str, TextIO],
+        fileobj: str | TextIO,
         bucket: str,
         key: str,
-        extra_args: Optional[Dict[str, Any]] = ...,
-        subscribers: Optional[List[BaseSubscriber]] = ...,
+        extra_args: Optional[dict[str, Any]] = ...,
+        subscribers: Optional[list[BaseSubscriber]] = ...,
     ) -> TransferFuture: ...
     def download(
         self,
         bucket: str,
         key: str,
-        fileobj: Union[str, TextIO],
-        extra_args: Optional[Dict[str, Any]] = ...,
-        subscribers: Optional[List[BaseSubscriber]] = ...,
+        fileobj: str | TextIO,
+        extra_args: Optional[dict[str, Any]] = ...,
+        subscribers: Optional[list[BaseSubscriber]] = ...,
     ) -> TransferFuture: ...
     def copy(
         self,
-        copy_source: Dict[str, str],
+        copy_source: dict[str, str],
         bucket: str,
         key: str,
-        extra_args: Optional[Dict[str, Any]] = ...,
-        subscribers: Optional[List[BaseSubscriber]] = ...,
+        extra_args: Optional[dict[str, Any]] = ...,
+        subscribers: Optional[list[BaseSubscriber]] = ...,
         source_client: S3Client = ...,
     ) -> TransferFuture: ...
     def delete(
         self,
         bucket: str,
         key: str,
-        extra_args: Optional[Dict[str, Any]] = ...,
-        subscribers: Optional[List[BaseSubscriber]] = ...,
+        extra_args: Optional[dict[str, Any]] = ...,
+        subscribers: Optional[list[BaseSubscriber]] = ...,
     ) -> TransferFuture: ...
     def __enter__(self) -> TransferConfig: ...
     def __exit__(
         self,
-        exc_type: Optional[Type[BaseException]],
+        exc_type: Optional[type[BaseException]],
         exc_value: Optional[BaseException],
         *args: Any,
     ) -> None: ...
@@ -122,8 +109,8 @@ _T = TypeVar("_T")
 class TransferCoordinatorController(Generic[_T]):
     def __init__(self) -> None: ...
     @property
-    def tracked_transfer_coordinators(self) -> Set[_T]: ...
+    def tracked_transfer_coordinators(self) -> set[_T]: ...
     def add_transfer_coordinator(self, transfer_coordinator: _T) -> None: ...
     def remove_transfer_coordinator(self, transfer_coordinator: _T) -> None: ...
-    def cancel(self, msg: str = ..., exc_type: Type[BaseException] = ...) -> None: ...
+    def cancel(self, msg: str = ..., exc_type: type[BaseException] = ...) -> None: ...
     def wait(self) -> None: ...
diff --git a/typings/s3transfer/utils.pyi b/typings/s3transfer/utils.pyi
index dbe287716..f7bfa1fa9 100644
--- a/typings/s3transfer/utils.pyi
+++ b/typings/s3transfer/utils.pyi
@@ -2,19 +2,7 @@
 
 from __future__ import annotations
 
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    ClassVar,
-    Dict,
-    List,
-    Optional,
-    Tuple,
-    Type,
-    TypeVar,
-    Union,
-)
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, TypeVar
 
 from typing_extensions import TypedDict
 
@@ -22,17 +10,19 @@ if TYPE_CHECKING:
     from logging import Logger
     from pathlib import Path
 
+    from typing_extensions import TypeAlias
+
     from .futures import TransferFuture
 
 _K = TypeVar("_K")
 _V = TypeVar("_V")
-_AnyPath = Union[Path, str]
+_AnyPath: TypeAlias = "Path | str"
 
 MAX_PARTS: int = ...
 MAX_SINGLE_UPLOAD_SIZE: int = 5 * 1024**3
 MIN_UPLOAD_CHUNKSIZE: int = ...
 logger: Logger = ...
-S3_RETRYABLE_DOWNLOAD_ERRORS: Tuple[Type[Exception], ...] = ...
+S3_RETRYABLE_DOWNLOAD_ERRORS: tuple[type[Exception], ...] = ...
 
 def random_file_extension(num_digits: int = ...) -> str: ...
 def signal_not_transferring(request: Any, operation_name: str, **kwargs: Any) -> None: ...
@@ -43,18 +33,18 @@ def calculate_range_parameter(
 ) -> str: ...
 def get_callbacks(
     transfer_future: TransferFuture, callback_type: str
-) -> List[Callable[..., Any]]: ...
+) -> list[Callable[..., Any]]: ...
 def invoke_progress_callbacks(
-    callbacks: List[Callable[..., Any]], bytes_transferred: int
+    callbacks: list[Callable[..., Any]], bytes_transferred: int
 ) -> None: ...
-def get_filtered_dict(original_dict: Dict[_K, _V], whitelisted_keys: List[str]) -> Dict[_K, _V]: ...
+def get_filtered_dict(original_dict: dict[_K, _V], whitelisted_keys: list[str]) -> dict[_K, _V]: ...
 
 _CopySource = TypedDict("_CopySource", Bucket=str, Key=str)
 
 class CallArgs:
     bucket: str
     copy_source: _CopySource
-    extra_args: Dict[str, Any]
+    extra_args: dict[str, Any]
     fileobj: _AnyPath
     key: str
     def __init__(self, **kwargs: Any) -> None: ...
@@ -80,15 +70,15 @@ class OSUtils:
         filename: _AnyPath,
         start_byte: int,
         size: int,
-        callbacks: List[Callable[..., Any]],
+        callbacks: list[Callable[..., Any]],
     ) -> ReadFileChunk: ...
     def open_file_chunk_reader_from_fileobj(
         self,
         fileobj: _AnyPath,
         chunk_size: int,
         full_file_size: int,
-        callbacks: List[Callable[..., Any]],
-        close_callbacks: Optional[List[Callable[..., Any]]] = ...,
+        callbacks: list[Callable[..., Any]],
+        close_callbacks: Optional[list[Callable[..., Any]]] = ...,
     ) -> ReadFileChunk: ...
     def open(self, filename: _AnyPath, mode: str) -> Any: ...
     def remove_file(self, filename: _AnyPath) -> None: ...


Test patch:
diff --git a/runway/cfngin/blueprints/testutil.py b/runway/cfngin/blueprints/testutil.py
index 363b11944..625cbbaad 100644
--- a/runway/cfngin/blueprints/testutil.py
+++ b/runway/cfngin/blueprints/testutil.py
@@ -133,7 +133,7 @@ def test_generator(
                 glob(f"{self.classdir}/{directory}/{self.yaml_filename}")  # noqa: PTH207
             )
 
-        class ConfigTest(self.base_class):  # type: ignore
+        class ConfigTest(self.base_class):
             """Config test."""
 
             context: CfnginContext
@@ -149,7 +149,7 @@ def __init__(
                 self.stack = stack
                 self.description = f"{stack.name} ({filepath})"
 
-            def __call__(self) -> None:
+            def __call__(self) -> None:  # pyright: ignore[reportIncompatibleMethodOverride]
                 """Run when the class instance is called directly."""
                 # Use the context property of the baseclass, if present.
                 # If not, default to a basic context.
diff --git a/runway/config/components/runway/_test_def.py b/runway/config/components/runway/_test_def.py
index 3ac48aa42..fb6e6cd18 100644
--- a/runway/config/components/runway/_test_def.py
+++ b/runway/config/components/runway/_test_def.py
@@ -1,76 +1,31 @@
 """Runway config test definition."""
 
-# ruff: noqa: UP006, UP035
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Dict, Generic, Tuple, TypeVar, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 
 from ....variables import Variable
-from ...models.runway import (
-    CfnLintRunwayTestArgs,
-    CfnLintRunwayTestDefinitionModel,
-    RunwayTestDefinitionModel,
-    ScriptRunwayTestArgs,
-    ScriptRunwayTestDefinitionModel,
-    ValidRunwayTestTypeValues,
-    YamlLintRunwayTestDefinitionModel,
-)
+from ...models.runway import RunwayTestDefinitionModel, ValidRunwayTestTypeValues
 from .base import ConfigComponentDefinition
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal
+    from typing_extensions import Self
 
-    from ...models.base import ConfigProperty
 
-
-_DataModel = TypeVar(
-    "_DataModel",
-    CfnLintRunwayTestDefinitionModel,
-    RunwayTestDefinitionModel,
-    ScriptRunwayTestDefinitionModel,
-    YamlLintRunwayTestDefinitionModel,
-)
-
-
-class RunwayTestDefinition(Generic[_DataModel], ConfigComponentDefinition):
+class RunwayTestDefinition(ConfigComponentDefinition[RunwayTestDefinitionModel]):
     """Runway test definition."""
 
-    args: Union[Dict[str, Any], ConfigProperty]
+    args: dict[str, Any]
     name: str
     required: bool
-    type: ValidRunwayTestTypeValues
+    type: ClassVar[ValidRunwayTestTypeValues]
 
-    _data: RunwayTestDefinitionModel
-    _supports_vars: Tuple[str, ...] = ("args", "required")
+    _supports_vars: tuple[str, ...] = ("args", "required")
 
-    def __init__(self, data: _DataModel) -> None:
+    def __init__(self, data: RunwayTestDefinitionModel) -> None:
         """Instantiate class."""
         super().__init__(data)
 
-    def __new__(
-        cls,
-        data: _DataModel,
-    ) -> RunwayTestDefinition[_DataModel]:
-        """Create a new instance of a class.
-
-        Returns:
-            Correct subclass of RunwayTestDefinition for the given data.
-
-        """
-        if cls is not RunwayTestDefinition:
-            return super().__new__(cls)
-        if isinstance(data, CfnLintRunwayTestDefinitionModel):
-            return super().__new__(CfnLintRunwayTestDefinition)  # type: ignore
-        if isinstance(data, ScriptRunwayTestDefinitionModel):
-            return super().__new__(ScriptRunwayTestDefinition)  # type: ignore
-        if isinstance(data, YamlLintRunwayTestDefinitionModel):
-            return super().__new__(YamlLintRunwayTestDefinition)  # type: ignore
-        raise TypeError(
-            "expected data of type CfnLintRunwayTestDefinitionModel, "
-            "ScriptRunwayTestDefinitionModel, or YamlLintRunwayTestDefinitionModel; "
-            f"got {type(data)}"
-        )
-
     def _register_variable(self, var_name: str, var_value: Any) -> None:
         """Register a variable.
 
@@ -86,73 +41,11 @@ def _register_variable(self, var_name: str, var_value: Any) -> None:
         )
 
     @classmethod
-    def parse_obj(cls, obj: Any) -> RunwayTestDefinition[_DataModel]:
-        """Parse a python object into this class.
-
-        Args:
-            obj: The object to parse.
-
-        """
-        return cls(RunwayTestDefinitionModel.parse_obj(obj))
-
-
-class CfnLintRunwayTestDefinition(RunwayTestDefinition[CfnLintRunwayTestDefinitionModel]):
-    """Runway cfn-lint test definition."""
-
-    args: CfnLintRunwayTestArgs
-    type: Literal["cfn-lint"] = "cfn-lint"
-
-    def __init__(self, data: CfnLintRunwayTestDefinitionModel) -> None:
-        """Instantiate class."""
-        super().__init__(data)
-
-    @classmethod
-    def parse_obj(cls, obj: Any) -> CfnLintRunwayTestDefinition:
-        """Parse a python object into this class.
-
-        Args:
-            obj: The object to parse.
-
-        """
-        return cls(CfnLintRunwayTestDefinitionModel.parse_obj(obj))
-
-
-class ScriptRunwayTestDefinition(RunwayTestDefinition[ScriptRunwayTestDefinitionModel]):
-    """Runway script test definition."""
-
-    args: ScriptRunwayTestArgs
-    type: Literal["script"] = "script"
-
-    def __init__(self, data: ScriptRunwayTestDefinitionModel) -> None:
-        """Instantiate class."""
-        super().__init__(data)
-
-    @classmethod
-    def parse_obj(cls, obj: Any) -> ScriptRunwayTestDefinition:
-        """Parse a python object into this class.
-
-        Args:
-            obj: The object to parse.
-
-        """
-        return cls(ScriptRunwayTestDefinitionModel.parse_obj(obj))
-
-
-class YamlLintRunwayTestDefinition(RunwayTestDefinition[YamlLintRunwayTestDefinitionModel]):
-    """Runway yamllint test definition."""
-
-    type: Literal["yamllint"] = "yamllint"
-
-    def __init__(self, data: YamlLintRunwayTestDefinitionModel) -> None:
-        """Instantiate class."""
-        super().__init__(data)
-
-    @classmethod
-    def parse_obj(cls, obj: Any) -> YamlLintRunwayTestDefinition:
+    def parse_obj(cls: type[Self], obj: object) -> Self:
         """Parse a python object into this class.
 
         Args:
             obj: The object to parse.
 
         """
-        return cls(YamlLintRunwayTestDefinitionModel.parse_obj(obj))
+        return cls(RunwayTestDefinitionModel.model_validate(obj))
diff --git a/runway/config/models/runway/_builtin_tests.py b/runway/config/models/runway/_builtin_tests.py
index 2197cf23d..cb4d8af0e 100644
--- a/runway/config/models/runway/_builtin_tests.py
+++ b/runway/config/models/runway/_builtin_tests.py
@@ -2,177 +2,45 @@
 
 from __future__ import annotations
 
-# ruff: noqa: UP006, UP035
-from typing import TYPE_CHECKING, Any, Dict, List, Union, cast
+from typing import Annotated, Any
 
-from pydantic import Extra, Field, validator
+from pydantic import ConfigDict, Field, field_validator
 from typing_extensions import Literal
 
 from .. import utils
 from ..base import ConfigProperty
 
-if TYPE_CHECKING:
-    from typing import Callable
-
 ValidRunwayTestTypeValues = Literal["cfn-lint", "script", "yamllint"]
 
 
 class RunwayTestDefinitionModel(ConfigProperty):
     """Model for a Runway test definition."""
 
-    args: Union[Dict[str, Any], ConfigProperty, str] = Field(
-        default={},
-        title="Arguments",
-        description="Arguments to be passed to the test. Support varies by test type.",
-    )
-    name: str = Field(default="test-name", description="Name of the test.")
-    required: Union[bool, str] = Field(
-        default=False,
-        description="Whether the test must pass for subsequent tests to be run.",
-    )
-    type: ValidRunwayTestTypeValues
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        schema_extra = {
+    model_config = ConfigDict(
+        extra="ignore",
+        json_schema_extra={
             "description": "Tests that can be run via the 'test' command.",
-        }
-        title = "Runway Test Definition"
-        use_enum_values = True
-
-    def __new__(cls, **kwargs: Any) -> RunwayTestDefinitionModel:  # noqa: PYI034
-        """Create a new instance of a class.
-
-        Returns:
-            Correct subclass of RunwayTestDefinition for the given data.
-
-        """
-        test_type = kwargs.get("type")
-        if cls is RunwayTestDefinitionModel:
-            if test_type == "cfn-lint":
-                return super().__new__(CfnLintRunwayTestDefinitionModel)  # type: ignore
-            if test_type == "script":
-                return super().__new__(ScriptRunwayTestDefinitionModel)  # type: ignore
-            if test_type == "yamllint":
-                return super().__new__(YamlLintRunwayTestDefinitionModel)  # type: ignore
-        return super().__new__(cls)  # type: ignore
-
-    # TODO (kyle): add regex to schema
-    _validate_string_is_lookup = validator(  # type: ignore
-        "args", "required", allow_reuse=True, pre=True
-    )(utils.validate_string_is_lookup)
-
-
-class CfnLintRunwayTestArgs(ConfigProperty):
-    """Model for the args of a cfn-lint test."""
-
-    cli_args: Union[List[str], str] = Field(
-        default=[],
-        title="CLI Arguments",
-        description="Array of arguments to pass to the cfn-lint CLI.",
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
-            "description": "Arguments supported by the cfn-lint test.",
-        }
-        title = "cfn-lint Runway Test Arguments"
+        },
+        title="Runway Test Definition",
+        use_enum_values=True,
+        validate_assignment=True,
+        validate_default=True,
+    )
+
+    args: Annotated[
+        dict[str, Any] | str,
+        Field(
+            title="Arguments",
+            description="Arguments to be passed to the test. Support varies by test type.",
+        ),
+    ] = {}
+    name: Annotated[str, Field(description="Name of the test.")] = "test-name"
+    required: Annotated[
+        bool | str, Field(description="Whether the test must pass for subsequent tests to be run.")
+    ] = False
+    type: ValidRunwayTestTypeValues
 
     # TODO (kyle): add regex to schema
-    _validate_string_is_lookup = validator("cli_args", allow_reuse=True, pre=True)(  # type: ignore
+    _validate_string_is_lookup = field_validator("args", "required", mode="before")(
         utils.validate_string_is_lookup
     )
-
-
-class CfnLintRunwayTestDefinitionModel(RunwayTestDefinitionModel):
-    """Model for a cfn-lint test definition."""
-
-    args: CfnLintRunwayTestArgs = Field(
-        default=CfnLintRunwayTestArgs(),
-        title="Arguments",
-        description="Arguments to be passed to the test.",
-    )
-    name: str = Field(default="cfn-lint", description="Name of the test.")
-    required: Union[bool, str] = Field(
-        default=False,
-        description="Whether the test must pass for subsequent tests to be run.",
-    )
-    type: Literal["cfn-lint"] = Field(default="cfn-lint", description="The type of test to run.")
-
-    class Config(RunwayTestDefinitionModel.Config):
-        """Model configuration."""
-
-        schema_extra = {
-            "description": "Test using cfn-lint.",
-        }
-        title = "cfn-lint Test"
-
-
-class ScriptRunwayTestArgs(ConfigProperty):
-    """Model for the args of a script test."""
-
-    commands: Union[List[str], str] = Field(
-        default=[], description="Array of commands that will be run for this test."
-    )
-
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-        schema_extra = {
-            "description": "Arguments supported by the script test.",
-        }
-        title = "Script Runway Test Arguments"
-
-    # TODO (kyle): add regex to schema
-    _validate_string_is_lookup = cast(
-        "classmethod[Callable[..., Any]]",
-        validator("commands", allow_reuse=True, pre=True)(utils.validate_string_is_lookup),
-    )
-
-
-class ScriptRunwayTestDefinitionModel(RunwayTestDefinitionModel):
-    """Model for a script test definition."""
-
-    args: ScriptRunwayTestArgs = Field(
-        default=ScriptRunwayTestArgs(),
-        title="Arguments",
-        description="Arguments to be passed to the test.",
-    )
-    name: str = Field(default="script", description="Name of the test.")
-    required: Union[bool, str] = Field(
-        default=False,
-        description="Whether the test must pass for subsequent tests to be run.",
-    )
-    type: Literal["script"] = Field(default="script", description="The type of test to run.")
-
-    class Config(RunwayTestDefinitionModel.Config):
-        """Model configuration."""
-
-        schema_extra = {
-            "description": "Test using a custom script.",
-        }
-        title = "Script Test"
-
-
-class YamlLintRunwayTestDefinitionModel(RunwayTestDefinitionModel):
-    """Model for a yamllint test definition."""
-
-    name: str = Field(default="yamllint", description="Name of the test.")
-    required: Union[bool, str] = Field(
-        default=False,
-        description="Whether the test must pass for subsequent tests to be run.",
-    )
-    type: Literal["yamllint"] = Field(default="yamllint", description="The type of test to run.")
-
-    class Config(RunwayTestDefinitionModel.Config):
-        """Model configuration."""
-
-        schema_extra = {
-            "description": "Test using yamllint.",
-        }
-        title = "yamllint Test"
diff --git a/tests/factories.py b/tests/factories.py
index a9cafd510..0bd677251 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -1,6 +1,5 @@
 """Test classes."""
 
-# pyright: basic, reportIncompatibleMethodOverride=none
 from __future__ import annotations
 
 import os  # imports os
diff --git a/tests/functional/cfngin/fixtures/blueprints/_lambda_function.py b/tests/functional/cfngin/fixtures/blueprints/_lambda_function.py
index 927a33774..7b6ed5559 100644
--- a/tests/functional/cfngin/fixtures/blueprints/_lambda_function.py
+++ b/tests/functional/cfngin/fixtures/blueprints/_lambda_function.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, ClassVar, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 
 import awacs.awslambda
 import awacs.dynamodb
@@ -150,7 +150,7 @@ def lambda_function(self) -> awslambda.Function:
         return func
 
     @cached_property
-    def lambda_layer(self) -> Optional[awslambda.LayerVersion]:
+    def lambda_layer(self) -> awslambda.LayerVersion | None:
         """AWS Lambda Layer."""
         create_layer = bool(self.variables["LayerContent"].S3Bucket)
         kwargs = {
diff --git a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_mysql/index.py b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_mysql/index.py
index a3009272d..39c76aea1 100644
--- a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_mysql/index.py
+++ b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_mysql/index.py
@@ -24,7 +24,7 @@ def handler(event: dict[str, Any], context: object) -> LambdaResponse:  # noqa:
                     str(path.relative_to(PACKAGE_DIR))
                     for path in sorted(PACKAGE_DIR.rglob("*"), reverse=True)
                 ],
-                "mysqlclient": [i[0] for i in inspect.getmembers(MySQLdb)],  # type: ignore
+                "mysqlclient": [i[0] for i in inspect.getmembers(MySQLdb)],
             },
             "message": None,
             "status": "success",
diff --git a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_xmlsec/index.py b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_xmlsec/index.py
index 1eaa4d45a..7f9962bbf 100644
--- a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_xmlsec/index.py
+++ b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/docker_xmlsec/index.py
@@ -25,8 +25,8 @@ def handler(event: dict[str, Any], context: object) -> LambdaResponse:  # noqa:
                     str(path.relative_to(PACKAGE_DIR))
                     for path in sorted(PACKAGE_DIR.rglob("*"), reverse=True)
                 ],
-                "lxml": [i[0] for i in inspect.getmembers(lxml)],  # type: ignore
-                "xmlsec": [i[0] for i in inspect.getmembers(xmlsec)],  # type: ignore
+                "lxml": [i[0] for i in inspect.getmembers(lxml)],
+                "xmlsec": [i[0] for i in inspect.getmembers(xmlsec)],
             },
             "message": None,
             "status": "success",
diff --git a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/local_xmlsec_layer/index.py b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/local_xmlsec_layer/index.py
index d52093dc7..7a820dc7d 100644
--- a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/local_xmlsec_layer/index.py
+++ b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/local_xmlsec_layer/index.py
@@ -26,8 +26,8 @@ def handler(event: dict[str, Any], context: object) -> LambdaResponse:  # noqa:
                     str(path.relative_to(PACKAGE_DIR))
                     for path in sorted(PACKAGE_DIR.rglob("*"), reverse=True)
                 ],
-                "lxml": [i[0] for i in inspect.getmembers(lxml)],  # type: ignore
-                "xmlsec": [i[0] for i in inspect.getmembers(xmlsec)],  # type: ignore
+                "lxml": [i[0] for i in inspect.getmembers(lxml)],
+                "xmlsec": [i[0] for i in inspect.getmembers(xmlsec)],
                 "opt_dir_contents": [
                     str(path) for path in sorted(OPT_DIR.rglob("*"), reverse=True)
                 ],
diff --git a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/type_defs.py b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/type_defs.py
index 53e20f81e..7fbcfe13b 100644
--- a/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/type_defs.py
+++ b/tests/functional/cfngin/hooks/test_awslambda/sample_app/src/type_defs.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Literal, Optional, TypedDict  # type: ignore
+from typing import Any, Literal, TypedDict
 
 
 class _LambdaResponseOptional(TypedDict, total=False):
@@ -16,7 +16,7 @@ class _LambdaResponseRequired(TypedDict):
 
     code: int
     data: dict[str, Any]
-    message: Optional[str]
+    message: str | None
     status: Literal["error", "success"]
 
 
diff --git a/tests/functional/cfngin/hooks/test_awslambda/test_runner.py b/tests/functional/cfngin/hooks/test_awslambda/test_runner.py
index a52daa5af..e308c04aa 100644
--- a/tests/functional/cfngin/hooks/test_awslambda/test_runner.py
+++ b/tests/functional/cfngin/hooks/test_awslambda/test_runner.py
@@ -5,10 +5,10 @@
 import json
 import shutil
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 
 import pytest
-from pydantic import root_validator
+from pydantic import model_validator
 
 from runway._cli import cli
 from runway.compat import cached_property
@@ -22,10 +22,11 @@
     from mypy_boto3_cloudformation.client import CloudFormationClient
     from mypy_boto3_cloudformation.type_defs import StackTypeDef
     from mypy_boto3_lambda.client import LambdaClient
-    from sample_app.src.type_defs import LambdaResponse
 
     from runway.context import RunwayContext
 
+    from .sample_app.src.type_defs import LambdaResponse
+
 AWS_REGION = "us-east-1"
 PYTHON_RUNTIME = "python3.10"
 STACK_PREFIX = "test-awslambda"
@@ -54,22 +55,23 @@
 class AwslambdaStackOutputs(BaseModel):
     """Outputs of a Stack used for testing the awslambda hook."""
 
-    CodeImageUri: Optional[str] = None
+    CodeImageUri: str | None = None
     CodeS3Bucket: str
     CodeS3Key: str
-    CodeS3ObjectVersion: Optional[str] = None
-    CodeZipFile: Optional[str] = None
+    CodeS3ObjectVersion: str | None = None
+    CodeZipFile: str | None = None
     LambdaFunction: str
     LambdaFunctionArn: str
     LambdaRole: str
-    LayerContentS3Bucket: Optional[str] = None
-    LayerContentS3Key: Optional[str] = None
-    LayerContentS3ObjectVersion: Optional[str] = None
-    LayerVersion: Optional[str] = None
+    LayerContentS3Bucket: str | None = None
+    LayerContentS3Key: str | None = None
+    LayerContentS3ObjectVersion: str | None = None
+    LayerVersion: str | None = None
     Runtime: str
 
-    @root_validator(allow_reuse=True, pre=True)  # type: ignore
-    def _convert_null_to_none(cls, values: dict[str, Any]) -> dict[str, Any]:  # noqa: N805
+    @model_validator(mode="before")
+    @classmethod
+    def _convert_null_to_none(cls, values: dict[str, Any]) -> dict[str, Any]:
         """Convert ``null`` to ``NoneType``."""
 
         def _handle_null(v: Any) -> Any:
@@ -101,7 +103,7 @@ def client(self) -> LambdaClient:
     @cached_property
     def outputs(self) -> AwslambdaStackOutputs:
         """Stack outputs."""
-        return AwslambdaStackOutputs.parse_obj(
+        return AwslambdaStackOutputs.model_validate(
             {
                 output["OutputKey"]: output["OutputValue"]
                 for output in self.stack.get("Outputs", [])
@@ -119,12 +121,12 @@ def stack(self) -> StackTypeDef:
             )
         return stacks[0]
 
-    def invoke(self, *, payload: Optional[str] = None) -> LambdaResponse:
+    def invoke(self, *, payload: str | None = None) -> LambdaResponse:
         """Invoke the Lambda Function."""
         response = self.client.invoke(
             FunctionName=self.outputs.LambdaFunction,
             InvocationType="RequestResponse",
-            **{"Payload": payload} if payload else {},
+            **{"Payload": payload} if payload else {},  # pyright: ignore[reportArgumentType]
         )
         if "Payload" in response:
             return json.load(response["Payload"])
diff --git a/tests/functional/cfngin/test_duplicate_stack/test_runner.py b/tests/functional/cfngin/test_duplicate_stack/test_runner.py
index ee4b4b173..7bf61c63a 100644
--- a/tests/functional/cfngin/test_duplicate_stack/test_runner.py
+++ b/tests/functional/cfngin/test_duplicate_stack/test_runner.py
@@ -34,11 +34,7 @@ def test_deploy_exit_code(deploy_result: Result) -> None:
 @pytest.mark.order(after="test_deploy_exit_code")
 def test_deploy_log_messages(deploy_result: Result) -> None:
     """Test deploy log messages."""
-    expected_lines = [
-        "[runway] 1 validation error for CfnginConfigDefinitionModel",
-        "stacks",
-        "  Duplicate stack vpc found at index 0 (type=value_error)",
-    ]
+    expected_lines = ["[runway] 1 validation error for CFNgin Config File", "stacks"]
     expected = "\n".join(expected_lines)
     assert expected in deploy_result.stdout, (
         "stdout does not match expected\n\nEXPECTED:\n"
diff --git a/tests/integration/cli/commands/test_destroy.py b/tests/integration/cli/commands/test_destroy.py
index ff60a199f..deb8a38e4 100644
--- a/tests/integration/cli/commands/test_destroy.py
+++ b/tests/integration/cli/commands/test_destroy.py
@@ -42,7 +42,7 @@ def test_destroy(cd_tmp_path: Path, cp_config: CpConfigTypeDef, mocker: MockerFi
     assert isinstance(mock_runway.call_args.args[1], RunwayContext)
 
     mock_runway.reverse_deployments.assert_called_once()
-    assert len(mock_runway.reverse_deployments.call_args.args[0]) == 1  # type: ignore
+    assert len(mock_runway.reverse_deployments.call_args.args[0]) == 1
     inst = mock_runway.return_value
     inst.destroy.assert_called_once_with(mock_runway.reverse_deployments.return_value)
 
diff --git a/tests/integration/cli/commands/test_init.py b/tests/integration/cli/commands/test_init.py
index b188d06b4..ccc9010d8 100644
--- a/tests/integration/cli/commands/test_init.py
+++ b/tests/integration/cli/commands/test_init.py
@@ -64,13 +64,12 @@ def test_init_handle_validation_error(
         f"{MODULE}.Runway",
         spec=Runway,
         spec_set=True,
-        init=Mock(side_effect=ValidationError([], Mock())),  # type: ignore
+        init=Mock(side_effect=ValidationError),
     )
     cp_config("min_required", cd_tmp_path)
     runner = CliRunner()
     result = runner.invoke(cli, ["init"])
     assert result.exit_code == 1
-    assert "ValidationError" in result.output
 
 
 def test_init_handle_config_not_found(
diff --git a/tests/integration/cli/commands/test_schema_cfngin.py b/tests/integration/cli/commands/test_schema_cfngin.py
index 7dbcfe1ac..7164b4115 100644
--- a/tests/integration/cli/commands/test_schema_cfngin.py
+++ b/tests/integration/cli/commands/test_schema_cfngin.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import json
 from typing import TYPE_CHECKING
 
 from click.testing import CliRunner
@@ -17,14 +18,20 @@ def test_schema_cfngin() -> None:
     """Test ``runway schema cfngin``."""
     result = CliRunner().invoke(cli, ["schema", "cfngin"])
     assert result.exit_code == 0
-    assert result.output == CfnginConfigDefinitionModel.schema_json(indent=4) + "\n"
+    assert (
+        result.output
+        == json.dumps(CfnginConfigDefinitionModel.model_json_schema(), indent=4) + "\n"
+    )
 
 
 def test_schema_cfngin_indent() -> None:
     """Test ``runway schema cfngin --indent 2``."""
     result = CliRunner().invoke(cli, ["schema", "cfngin", "--indent", "2"])
     assert result.exit_code == 0
-    assert result.output == CfnginConfigDefinitionModel.schema_json(indent=2) + "\n"
+    assert (
+        result.output
+        == json.dumps(CfnginConfigDefinitionModel.model_json_schema(), indent=2) + "\n"
+    )
 
 
 def test_schema_cfngin_output(cd_tmp_path: Path) -> None:
@@ -34,4 +41,7 @@ def test_schema_cfngin_output(cd_tmp_path: Path) -> None:
     assert result.exit_code == 0
     assert str(file_path) in result.output
     assert file_path.is_file()
-    assert file_path.read_text() == CfnginConfigDefinitionModel.schema_json(indent=4) + "\n"
+    assert (
+        file_path.read_text()
+        == json.dumps(CfnginConfigDefinitionModel.model_json_schema(), indent=4) + "\n"
+    )
diff --git a/tests/integration/cli/commands/test_schema_runway.py b/tests/integration/cli/commands/test_schema_runway.py
index e64a99bd1..612ed413b 100644
--- a/tests/integration/cli/commands/test_schema_runway.py
+++ b/tests/integration/cli/commands/test_schema_runway.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import json
 from typing import TYPE_CHECKING
 
 from click.testing import CliRunner
@@ -17,14 +18,20 @@ def test_schema_runway() -> None:
     """Test ``runway schema runway``."""
     result = CliRunner().invoke(cli, ["schema", "runway"])
     assert result.exit_code == 0
-    assert result.output == RunwayConfigDefinitionModel.schema_json(indent=4) + "\n"
+    assert (
+        result.output
+        == json.dumps(RunwayConfigDefinitionModel.model_json_schema(), indent=4) + "\n"
+    )
 
 
 def test_schema_runway_indent() -> None:
     """Test ``runway schema runway --indent 2``."""
     result = CliRunner().invoke(cli, ["schema", "runway", "--indent", "2"])
     assert result.exit_code == 0
-    assert result.output == RunwayConfigDefinitionModel.schema_json(indent=2) + "\n"
+    assert (
+        result.output
+        == json.dumps(RunwayConfigDefinitionModel.model_json_schema(), indent=2) + "\n"
+    )
 
 
 def test_schema_runway_output(cd_tmp_path: Path) -> None:
@@ -34,4 +41,7 @@ def test_schema_runway_output(cd_tmp_path: Path) -> None:
     assert result.exit_code == 0
     assert str(file_path) in result.output
     assert file_path.is_file()
-    assert file_path.read_text() == RunwayConfigDefinitionModel.schema_json(indent=4) + "\n"
+    assert (
+        file_path.read_text()
+        == json.dumps(RunwayConfigDefinitionModel.model_json_schema(), indent=4) + "\n"
+    )
diff --git a/tests/unit/cfngin/actions/conftest.py b/tests/unit/cfngin/actions/conftest.py
index 829d9d9d6..2a22d3288 100644
--- a/tests/unit/cfngin/actions/conftest.py
+++ b/tests/unit/cfngin/actions/conftest.py
@@ -1,6 +1,5 @@
 """Pytest fixtures and plugins."""
 
-# pyright: basic
 from __future__ import annotations
 
 from datetime import datetime
diff --git a/tests/unit/cfngin/actions/test_base.py b/tests/unit/cfngin/actions/test_base.py
index e11b7d39d..920f826b0 100644
--- a/tests/unit/cfngin/actions/test_base.py
+++ b/tests/unit/cfngin/actions/test_base.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.actions.base."""
 
-# pyright: basic
 import unittest
 from unittest.mock import MagicMock, PropertyMock, patch
 
diff --git a/tests/unit/cfngin/actions/test_deploy.py b/tests/unit/cfngin/actions/test_deploy.py
index 101fbbd00..80383a03a 100644
--- a/tests/unit/cfngin/actions/test_deploy.py
+++ b/tests/unit/cfngin/actions/test_deploy.py
@@ -5,7 +5,7 @@
 import unittest
 from collections import namedtuple
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import MagicMock, PropertyMock, patch
 
 import pytest
@@ -116,7 +116,7 @@ class TestAction:
     )
     def test_upload_disabled(
         self,
-        bucket_name: Optional[str],
+        bucket_name: str | None,
         cfngin_context: CfnginContext,
         explicit: bool,
         expected: bool,
@@ -164,7 +164,7 @@ def setUp(self) -> None:
         )
 
     def _get_context(
-        self, extra_config_args: Optional[dict[str, Any]] = None, **kwargs: Any
+        self, extra_config_args: dict[str, Any] | None = None, **kwargs: Any
     ) -> CfnginContext:
         """Get context."""
         config: dict[str, Any] = {
@@ -445,8 +445,8 @@ def get_events(*_args: Any, **_kwargs: Any) -> list[dict[str, Any]]:
 
     def _advance(
         self,
-        new_provider_status: Optional[str],
-        expected_status: Optional[Status],
+        new_provider_status: str | None,
+        expected_status: Status | None,
         expected_reason: str,
     ) -> None:
         """Advance."""
diff --git a/tests/unit/cfngin/actions/test_destroy.py b/tests/unit/cfngin/actions/test_destroy.py
index c118b6ccd..48e5abeba 100644
--- a/tests/unit/cfngin/actions/test_destroy.py
+++ b/tests/unit/cfngin/actions/test_destroy.py
@@ -1,10 +1,9 @@
 """Tests for runway.cfngin.actions.destroy."""
 
-# pyright: basic
 from __future__ import annotations
 
 import unittest
-from typing import Any, Optional
+from typing import Any
 from unittest.mock import MagicMock, PropertyMock, patch
 
 from runway.cfngin.actions import destroy
@@ -38,7 +37,7 @@ def setUp(self) -> None:
         self.action = destroy.Action(self.context, cancel=MockThreadingEvent())  # type: ignore
 
     def _get_context(
-        self, extra_config_args: Optional[dict[str, Any]] = None, **kwargs: Any
+        self, extra_config_args: dict[str, Any] | None = None, **kwargs: Any
     ) -> CfnginContext:
         """Get context."""
         config = {
diff --git a/tests/unit/cfngin/actions/test_diff.py b/tests/unit/cfngin/actions/test_diff.py
index af271bc3d..5c217471e 100644
--- a/tests/unit/cfngin/actions/test_diff.py
+++ b/tests/unit/cfngin/actions/test_diff.py
@@ -1,12 +1,11 @@
 """Tests for runway.cfngin.actions.diff."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
 import unittest
 from operator import attrgetter
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 from unittest.mock import MagicMock, Mock, patch
 
 import pytest
@@ -49,7 +48,7 @@ def test_pre_run(
         self,
         mock_bucket_init: MagicMock,
         caplog: pytest.LogCaptureFixture,
-        bucket_name: Optional[str],
+        bucket_name: str | None,
         forbidden: bool,
         not_found: bool,
         cfngin_context: MockCfnginContext,
diff --git a/tests/unit/cfngin/blueprints/test_base.py b/tests/unit/cfngin/blueprints/test_base.py
index a6ba79324..d40545aac 100644
--- a/tests/unit/cfngin/blueprints/test_base.py
+++ b/tests/unit/cfngin/blueprints/test_base.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import json
-from typing import TYPE_CHECKING, Any, ClassVar, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 from unittest.mock import Mock
 
 import pytest
@@ -359,7 +359,7 @@ def test_to_parameter_value(self) -> None:
             (1, "1"),
         ],
     )
-    def test_value(self, expected: Union[list[str], str], provided: Any) -> None:
+    def test_value(self, expected: list[str] | str, provided: Any) -> None:
         """Test value."""
         assert CFNParameter("myParameter", provided).value == expected
 
diff --git a/tests/unit/cfngin/blueprints/test_raw.py b/tests/unit/cfngin/blueprints/test_raw.py
index 402870905..4fb37da1f 100644
--- a/tests/unit/cfngin/blueprints/test_raw.py
+++ b/tests/unit/cfngin/blueprints/test_raw.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.blueprints.raw."""
 
-# pyright: basic
 from __future__ import annotations
 
 import json
diff --git a/tests/unit/cfngin/blueprints/test_testutil.py b/tests/unit/cfngin/blueprints/test_testutil.py
index ee8dd8ee5..f94781ad1 100644
--- a/tests/unit/cfngin/blueprints/test_testutil.py
+++ b/tests/unit/cfngin/blueprints/test_testutil.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.blueprints.testutil."""
 
-# pyright: basic
 import unittest
 
 import pytest
diff --git a/tests/unit/cfngin/factories.py b/tests/unit/cfngin/factories.py
index ce1599417..9b60fd666 100644
--- a/tests/unit/cfngin/factories.py
+++ b/tests/unit/cfngin/factories.py
@@ -1,9 +1,8 @@
 """Factories for tests."""
 
-# pyright: basic
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, NamedTuple, Optional
+from typing import TYPE_CHECKING, Any, NamedTuple
 from unittest.mock import MagicMock
 
 from runway.cfngin.providers.aws.default import ProviderBuilder
@@ -25,7 +24,7 @@ class Lookup(NamedTuple):
 class MockThreadingEvent:
     """Mock thread events."""
 
-    def wait(self, timeout: Optional[int] = None) -> bool:  # noqa: ARG002
+    def wait(self, timeout: int | None = None) -> bool:  # noqa: ARG002
         """Mock wait method."""
         return False
 
@@ -33,13 +32,13 @@ def wait(self, timeout: Optional[int] = None) -> bool:  # noqa: ARG002
 class MockProviderBuilder(ProviderBuilder):
     """Mock provider builder."""
 
-    def __init__(self, *, provider: Provider, region: Optional[str] = None, **_: Any) -> None:
+    def __init__(self, *, provider: Provider, region: str | None = None, **_: Any) -> None:
         """Instantiate class."""
         self.provider = provider
         self.region = region
 
     def build(
-        self, *, profile: Optional[str] = None, region: Optional[str] = None  # noqa: ARG002
+        self, *, profile: str | None = None, region: str | None = None  # noqa: ARG002
     ) -> Provider:
         """Mock build method."""
         return self.provider
@@ -52,7 +51,7 @@ def mock_provider(**kwargs: Any) -> MagicMock:
 
 def mock_context(
     namespace: str = "default",
-    extra_config_args: Optional[dict[str, Any]] = None,
+    extra_config_args: dict[str, Any] | None = None,
     **kwargs: Any,
 ) -> CfnginContext:
     """Mock context."""
@@ -78,7 +77,7 @@ def generate_definition(
     return CfnginStackDefinitionModel(**definition)
 
 
-def mock_lookup(lookup_input: Any, lookup_type: str, raw: Optional[str] = None) -> Lookup:
+def mock_lookup(lookup_input: Any, lookup_type: str, raw: str | None = None) -> Lookup:
     """Mock lookup."""
     if raw is None:
         raw = f"{lookup_type} {lookup_input}"
diff --git a/tests/unit/cfngin/hooks/awslambda/models/test_args.py b/tests/unit/cfngin/hooks/awslambda/models/test_args.py
index 8bd2a6e15..9b6021a69 100644
--- a/tests/unit/cfngin/hooks/awslambda/models/test_args.py
+++ b/tests/unit/cfngin/hooks/awslambda/models/test_args.py
@@ -52,7 +52,7 @@ def test__validate_runtime_or_docker_docker_no_runtime(
             kwargs["file"] = dockerfile
         obj = AwsLambdaHookArgs(
             bucket_name="test-bucket",
-            docker=DockerOptions.parse_obj(kwargs),
+            docker=DockerOptions.model_validate(kwargs),
             source_code=tmp_path,
         )
         assert not obj.runtime
@@ -63,28 +63,26 @@ def test__validate_runtime_or_docker_docker_disabled(self, tmp_path: Path) -> No
         With ``runtime=None`` and ``docker.disabled=True``.
 
         """
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="runtime\n  Value error, runtime must be provided if docker.disabled is True",
+        ):
             AwsLambdaHookArgs(
                 bucket_name="test-bucket",
                 docker=DockerOptions(disabled=True),
                 source_code=tmp_path,
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("runtime",)
-        assert errors[0]["msg"] == "runtime must be provided if docker.disabled is True"
 
     def test__validate_runtime_or_docker_no_runtime_or_docker(self, tmp_path: Path) -> None:
         """Test _validate_runtime_or_docker no runtime or docker."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="runtime\n  Value error, docker.file, docker.image, or runtime is required",
+        ):
             AwsLambdaHookArgs(
                 bucket_name="test-bucket",
                 source_code=tmp_path,
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("runtime",)
-        assert errors[0]["msg"] == "docker.file, docker.image, or runtime is required"
 
     def test_field_defaults(self, tmp_path: Path) -> None:
         """Test field defaults."""
@@ -101,30 +99,28 @@ def test_source_code_is_file(self, tmp_path: Path) -> None:
         """Test source_code is file."""
         source_path = tmp_path / "foo"
         source_path.write_text("bar")
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="source_code\n  Path does not point to a directory",
+        ):
             AwsLambdaHookArgs(  # these are all required fields
                 bucket_name="test-bucket",
                 runtime="test",
                 source_code=source_path,
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("source_code",)
-        assert errors[0]["msg"] == f'path "{source_path}" does not point to a directory'
 
     def test_source_code_not_exist(self, tmp_path: Path) -> None:
         """Test source_code directory does not exist."""
         source_path = tmp_path / "foo"
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="source_code\n  Path does not point to a directory",
+        ):
             AwsLambdaHookArgs(  # these are all required fields
                 bucket_name="test-bucket",
                 runtime="test",
                 source_code=source_path,
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("source_code",)
-        assert errors[0]["msg"] == f'file or directory at path "{source_path}" does not exist'
 
 
 class TestPythonHookArgs:
diff --git a/tests/unit/cfngin/hooks/awslambda/models/test_responses.py b/tests/unit/cfngin/hooks/awslambda/models/test_responses.py
index 0484b49cd..7ac956fbf 100644
--- a/tests/unit/cfngin/hooks/awslambda/models/test_responses.py
+++ b/tests/unit/cfngin/hooks/awslambda/models/test_responses.py
@@ -13,7 +13,7 @@ class TestAwsLambdaHookDeployResponse:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             AwsLambdaHookDeployResponse(
                 bucket_name="test-bucket",
                 code_sha256="sha256",
@@ -21,7 +21,3 @@ def test_extra(self) -> None:
                 object_key="key",
                 runtime="test",
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
diff --git a/tests/unit/cfngin/hooks/awslambda/python_requirements/test__docker.py b/tests/unit/cfngin/hooks/awslambda/python_requirements/test__docker.py
index 42c37e662..71090c079 100644
--- a/tests/unit/cfngin/hooks/awslambda/python_requirements/test__docker.py
+++ b/tests/unit/cfngin/hooks/awslambda/python_requirements/test__docker.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 from unittest.mock import Mock
 
 import pytest
@@ -131,7 +131,7 @@ def test_python_version_not_found(self, mocker: MockerFixture) -> None:
         ],
     )
     def test_runtime(
-        self, expected: Optional[str], mocker: MockerFixture, version: Optional[Version]
+        self, expected: str | None, mocker: MockerFixture, version: Version | None
     ) -> None:
         """Test runtime."""
         mocker.patch.object(PythonDockerDependencyInstaller, "python_version", version)
diff --git a/tests/unit/cfngin/hooks/awslambda/test__python_hooks.py b/tests/unit/cfngin/hooks/awslambda/test__python_hooks.py
index 7ad5724da..7cd7a0060 100644
--- a/tests/unit/cfngin/hooks/awslambda/test__python_hooks.py
+++ b/tests/unit/cfngin/hooks/awslambda/test__python_hooks.py
@@ -35,7 +35,7 @@ class TestPythonFunction:
     def test___init__(self, args: PythonHookArgs) -> None:
         """Test __init__."""
         ctx = Mock()
-        obj = PythonFunction(ctx, **args.dict())
+        obj = PythonFunction(ctx, **args.model_dump())
         # only two attributes are being set currently
         assert obj.args == args
         assert obj.ctx == ctx
@@ -48,14 +48,14 @@ def test___init___raise_validation_error(self) -> None:
     def test_cleanup(self, args: PythonHookArgs, mocker: MockerFixture) -> None:
         """Test cleanup."""
         project = mocker.patch.object(PythonFunction, "project")
-        assert not PythonFunction(Mock(), **args.dict()).cleanup()
+        assert not PythonFunction(Mock(), **args.model_dump()).cleanup()
         project.cleanup.assert_called_once_with()
 
     def test_cleanup_on_error(self, args: PythonHookArgs, mocker: MockerFixture) -> None:
         """Test cleanup_on_error."""
         deployment_package = mocker.patch.object(PythonFunction, "deployment_package")
         project = mocker.patch.object(PythonFunction, "project")
-        assert not PythonFunction(Mock(), **args.dict()).cleanup_on_error()
+        assert not PythonFunction(Mock(), **args.model_dump()).cleanup_on_error()
         deployment_package.delete.assert_called_once_with()
         project.cleanup_on_error.assert_called_once_with()
 
@@ -64,22 +64,25 @@ def test_deployment_package(self, args: PythonHookArgs, mocker: MockerFixture) -
         deployment_package_class = mocker.patch(f"{MODULE}.PythonDeploymentPackage")
         project = mocker.patch.object(PythonFunction, "project", "project")
         assert (
-            PythonFunction(Mock(), **args.dict()).deployment_package
+            PythonFunction(Mock(), **args.model_dump()).deployment_package
             == deployment_package_class.init.return_value
         )
         deployment_package_class.init.assert_called_once_with(project, "function")
 
     def test_pre_deploy(self, args: PythonHookArgs, mocker: MockerFixture) -> None:
         """Test pre_deploy."""
-        model = Mock(dict=Mock(return_value="success"))
+        model = Mock(model_dump=Mock(return_value="success"))
         build_response = mocker.patch.object(PythonFunction, "build_response", return_value=(model))
         cleanup = mocker.patch.object(PythonFunction, "cleanup")
         cleanup_on_error = mocker.patch.object(PythonFunction, "cleanup_on_error")
         deployment_package = mocker.patch.object(PythonFunction, "deployment_package")
-        assert PythonFunction(Mock(), **args.dict()).pre_deploy() == model.dict.return_value
+        assert (
+            PythonFunction(Mock(), **args.model_dump()).pre_deploy()
+            == model.model_dump.return_value
+        )
         deployment_package.upload.assert_called_once_with()
         build_response.assert_called_once_with("deploy")
-        model.dict.assert_called_once_with(by_alias=True)
+        model.model_dump.assert_called_once_with(by_alias=True)
         cleanup_on_error.assert_not_called()
         cleanup.assert_called_once_with()
 
@@ -96,7 +99,7 @@ def test_pre_deploy_always_cleanup(self, args: PythonHookArgs, mocker: MockerFix
             Mock(upload=Mock(side_effect=Exception)),
         )
         with pytest.raises(Exception):  # noqa: B017, PT011
-            assert PythonFunction(Mock(), **args.dict()).pre_deploy()
+            assert PythonFunction(Mock(), **args.model_dump()).pre_deploy()
         deployment_package.upload.assert_called_once_with()
         build_response.assert_not_called()
         cleanup_on_error.assert_called_once_with()
@@ -106,7 +109,7 @@ def test_project(self, args: PythonHookArgs, mocker: MockerFixture) -> None:
         """Test project."""
         ctx = Mock()
         project_class = mocker.patch(f"{MODULE}.PythonProject")
-        assert PythonFunction(ctx, **args.dict()).project == project_class.return_value
+        assert PythonFunction(ctx, **args.model_dump()).project == project_class.return_value
         project_class.assert_called_once_with(args, ctx)
 
 
@@ -118,7 +121,7 @@ def test_deployment_package(self, args: PythonHookArgs, mocker: MockerFixture) -
         deployment_package_class = mocker.patch(f"{MODULE}.PythonDeploymentPackage")
         project = mocker.patch.object(PythonLayer, "project", "project")
         assert (
-            PythonLayer(Mock(), **args.dict()).deployment_package
+            PythonLayer(Mock(), **args.model_dump()).deployment_package
             == deployment_package_class.init.return_value
         )
         deployment_package_class.init.assert_called_once_with(project, "layer")
diff --git a/tests/unit/cfngin/hooks/awslambda/test_base_classes.py b/tests/unit/cfngin/hooks/awslambda/test_base_classes.py
index 4ecacde96..a07d67b73 100644
--- a/tests/unit/cfngin/hooks/awslambda/test_base_classes.py
+++ b/tests/unit/cfngin/hooks/awslambda/test_base_classes.py
@@ -124,13 +124,13 @@ def test_deployment_package(self) -> None:
 
     def test_plan(self, mocker: MockerFixture) -> None:
         """Test plan."""
-        response_obj = Mock(dict=Mock(return_value="success"))
+        response_obj = Mock(model_dump=Mock(return_value="success"))
         build_response = mocker.patch.object(
             AwsLambdaHook, "build_response", return_value=response_obj
         )
-        assert AwsLambdaHook(Mock()).plan() == response_obj.dict.return_value
+        assert AwsLambdaHook(Mock()).plan() == response_obj.model_dump.return_value
         build_response.assert_called_once_with("plan")
-        response_obj.dict.assert_called_once_with(by_alias=True)
+        response_obj.model_dump.assert_called_once_with(by_alias=True)
 
     def test_post_deploy(self, caplog: pytest.LogCaptureFixture) -> None:
         """Test post_deploy."""
diff --git a/tests/unit/cfngin/hooks/awslambda/test_deployment_package.py b/tests/unit/cfngin/hooks/awslambda/test_deployment_package.py
index 4c7d71d70..c8dc45784 100644
--- a/tests/unit/cfngin/hooks/awslambda/test_deployment_package.py
+++ b/tests/unit/cfngin/hooks/awslambda/test_deployment_package.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import zipfile
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import MagicMock, Mock, PropertyMock, call
 from urllib.parse import urlencode
 
@@ -501,7 +501,7 @@ def test_md5_checksum(self, mocker: MockerFixture, project: ProjectTypeAlias) ->
     def test_object_key(
         self,
         project: ProjectTypeAlias,
-        object_prefix: Optional[str],
+        object_prefix: str | None,
         usage_type: Literal["function", "layer"],
     ) -> None:
         """Test object_key."""
@@ -519,7 +519,7 @@ def test_object_key(
     @pytest.mark.parametrize("response, expected", [({}, None), ({"VersionId": "foo"}, "foo")])
     def test_object_version_id(
         self,
-        expected: Optional[str],
+        expected: str | None,
         mocker: MockerFixture,
         project: ProjectTypeAlias,
         response: dict[str, Any],
@@ -663,7 +663,7 @@ def test_code_sha256_raise_required_tag_not_found(
 
     @pytest.mark.parametrize("value", ["foobar", None, "foo,bar"])
     def test_compatible_architectures(
-        self, mocker: MockerFixture, project: ProjectTypeAlias, value: Optional[str]
+        self, mocker: MockerFixture, project: ProjectTypeAlias, value: str | None
     ) -> None:
         """Test compatible_architectures."""
         mocker.patch.object(
@@ -681,7 +681,7 @@ def test_compatible_architectures(
 
     @pytest.mark.parametrize("value", ["foobar", None, "foo,bar"])
     def test_compatible_runtimes(
-        self, mocker: MockerFixture, project: ProjectTypeAlias, value: Optional[str]
+        self, mocker: MockerFixture, project: ProjectTypeAlias, value: str | None
     ) -> None:
         """Test compatible_runtimes."""
         mocker.patch.object(
@@ -822,7 +822,7 @@ def test_head_404(
 
     @pytest.mark.parametrize("value", ["foobar", None])
     def test_license(
-        self, mocker: MockerFixture, project: ProjectTypeAlias, value: Optional[str]
+        self, mocker: MockerFixture, project: ProjectTypeAlias, value: str | None
     ) -> None:
         """Test license."""
         mocker.patch.object(
@@ -900,7 +900,7 @@ def test_object_tags(
     )
     def test_object_version_id(
         self,
-        expected: Optional[str],
+        expected: str | None,
         head: dict[str, str],
         mocker: MockerFixture,
         project: ProjectTypeAlias,
diff --git a/tests/unit/cfngin/hooks/awslambda/test_docker.py b/tests/unit/cfngin/hooks/awslambda/test_docker.py
index 6f8d5d8e6..bed13ad8e 100644
--- a/tests/unit/cfngin/hooks/awslambda/test_docker.py
+++ b/tests/unit/cfngin/hooks/awslambda/test_docker.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 from unittest.mock import Mock, call
 
 import pytest
@@ -87,9 +87,9 @@ def test_bind_mounts_cache_dir(self) -> None:
     def test_build_image(
         self,
         mocker: MockerFixture,
-        name: Optional[str],
+        name: str | None,
         pull: bool,
-        tag: Optional[str],
+        tag: str | None,
         tmp_path: Path,
     ) -> None:
         """Test build_image."""
@@ -219,10 +219,10 @@ def test_from_project_raise_docker_error(self, mocker: MockerFixture) -> None:
     )
     def test_image_build_image(
         self,
-        image: Optional[str],
+        image: str | None,
         mocker: MockerFixture,
-        name: Optional[str],
-        runtime: Optional[str],
+        name: str | None,
+        runtime: str | None,
     ) -> None:
         """Test image build image."""
         project = Mock(args=Mock(docker=Mock(file="foo", image=image), runtime=runtime))
@@ -240,10 +240,10 @@ def test_image_build_image(
     )
     def test_image_pull_image(
         self,
-        image: Optional[str],
+        image: str | None,
         mocker: MockerFixture,
         pull: bool,
-        runtime: Optional[str],
+        runtime: str | None,
     ) -> None:
         """Test image pull image."""
         project = Mock(args=Mock(docker=Mock(file=None, image=image, pull=pull), runtime=runtime))
@@ -290,7 +290,7 @@ def test_install(self, mocker: MockerFixture) -> None:
         obj = DockerDependencyInstaller(Mock(), client=Mock())
         assert not obj.install()
         run_command.assert_has_calls(
-            [  # type: ignore
+            [
                 call(pre_install_commands[0]),
                 call(install_commands[0]),
                 call(post_install_commands[0]),
@@ -331,7 +331,7 @@ def test_log_docker_msg_dict(self, level: int, mocker: MockerFixture) -> None:
             )
             == msgs[:-1]
         )
-        docker_logger.log.assert_has_calls([call(level, m) for m in msgs[:-1]])  # type: ignore
+        docker_logger.log.assert_has_calls([call(level, m) for m in msgs[:-1]])
 
     def test_post_install_commands(
         self,
@@ -438,7 +438,7 @@ def test_pull_image(
                 assert caplog.messages == [f"image not found; pulling docker image {name}..."]
 
     @pytest.mark.parametrize("command, level", [("foo", logging.DEBUG), ("bar", None)])
-    def test_run_command(self, command: str, level: Optional[int], mocker: MockerFixture) -> None:
+    def test_run_command(self, command: str, level: int | None, mocker: MockerFixture) -> None:
         """Test run_command."""
         container = Mock(
             logs=Mock(return_value="log-stream"),
diff --git a/tests/unit/cfngin/hooks/docker/image/test_build.py b/tests/unit/cfngin/hooks/docker/image/test__build.py
similarity index 85%
rename from tests/unit/cfngin/hooks/docker/image/test_build.py
rename to tests/unit/cfngin/hooks/docker/image/test__build.py
index e759829e6..1b04cde42 100644
--- a/tests/unit/cfngin/hooks/docker/image/test_build.py
+++ b/tests/unit/cfngin/hooks/docker/image/test__build.py
@@ -1,9 +1,8 @@
 """Test runway.cfngin.hooks.docker.image._build."""
 
-# pyright: basic
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 from unittest.mock import MagicMock
 
 import pytest
@@ -50,7 +49,7 @@ def test_build(cfngin_context: MockCfnginContext, mocker: MockerFixture, tmp_pat
     mock_logs = [{"stream": "log message\n"}, {"not-stream": "no log"}]
     mock_client = MagicMock(images=MagicMock(build=MagicMock(return_value=(mock_image, mock_logs))))
     args = ImageBuildArgs(path=tmp_path)
-    mocker.patch.object(ImageBuildArgs, "parse_obj", return_value=args)
+    mocker.patch.object(ImageBuildArgs, "model_validate", return_value=args)
     mocker.patch.object(DockerHookData, "client", mock_client)
     docker_hook_data = DockerHookData()
     mock_from_cfngin_context = mocker.patch.object(
@@ -60,9 +59,11 @@ def test_build(cfngin_context: MockCfnginContext, mocker: MockerFixture, tmp_pat
         DockerHookData, "update_context", return_value=docker_hook_data
     )
     cfngin_context.hook_data["docker"] = docker_hook_data
-    assert build(context=cfngin_context, **args.dict()) == docker_hook_data
+    assert build(context=cfngin_context, **args.model_dump()) == docker_hook_data
     mock_from_cfngin_context.assert_called_once_with(cfngin_context)
-    mock_client.images.build.assert_called_once_with(path=str(args.path), **args.docker.dict())
+    mock_client.images.build.assert_called_once_with(
+        path=str(args.path), **args.docker.model_dump()
+    )
     mock_image.tag.assert_called_once_with(None, tag="latest")
     mock_image.reload.assert_called_once()
     assert isinstance(docker_hook_data.image, DockerImage)
@@ -104,14 +105,14 @@ class TestImageBuildArgs:
     )
     def test__set_docker_dict(
         self,
-        expected: Optional[str],
-        repo: Optional[str],
-        tag: Optional[str],
+        expected: str | None,
+        repo: str | None,
+        tag: str | None,
         tmp_path: Path,
     ) -> None:
         """Test _set_docker."""
         assert (
-            ImageBuildArgs.parse_obj(
+            ImageBuildArgs.model_validate(
                 {
                     "docker": {"tag": tag} if tag else {},
                     "path": tmp_path,
@@ -127,9 +128,9 @@ def test__set_docker_dict(
     )
     def test__set_docker_model(
         self,
-        expected: Optional[str],
-        repo: Optional[str],
-        tag: Optional[str],
+        expected: str | None,
+        repo: str | None,
+        tag: str | None,
         tmp_path: Path,
     ) -> None:
         """Test _set_docker."""
@@ -150,7 +151,7 @@ def test__set_ecr_repo_from_dict(self, tmp_path: Path) -> None:
             "registry_alias": "bar",
             "aws_region": "us-west-2",
         }
-        obj = ImageBuildArgs.parse_obj({"path": tmp_path, "ecr_repo": args})
+        obj = ImageBuildArgs.model_validate({"path": tmp_path, "ecr_repo": args})
         assert obj.ecr_repo
         assert obj.ecr_repo.name == args["repo_name"]
         assert obj.ecr_repo.registry.account_id == args["account_id"]
@@ -164,16 +165,15 @@ def test__set_repo(self, tmp_path: Path) -> None:
     def test__set_repo_ecr(self, tmp_path: Path) -> None:
         """Test _set_repo ECR."""
         repo = ElasticContainerRegistryRepository(
-            repo_name="test",
+            name="test",
             registry=ElasticContainerRegistry(account_id="123456789012", aws_region="us-east-1"),
         )
         assert ImageBuildArgs(path=tmp_path, ecr_repo=repo).repo == repo.fqn
 
     def test__validate_dockerfile_raise_value_error(self, tmp_path: Path) -> None:
         """Test _validate_dockerfile raise ValueError."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="dockerfile\n  Value error, Dockerfile does not exist at path provided",
+        ):
             assert ImageBuildArgs(dockerfile="invalid", path=tmp_path, repo="something")
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("dockerfile",)
-        assert errors[0]["msg"].startswith("Dockerfile does not exist at path provided: ")
diff --git a/tests/unit/cfngin/hooks/docker/image/test_push.py b/tests/unit/cfngin/hooks/docker/image/test__push.py
similarity index 92%
rename from tests/unit/cfngin/hooks/docker/image/test_push.py
rename to tests/unit/cfngin/hooks/docker/image/test__push.py
index 57b128cc6..bca932b0f 100644
--- a/tests/unit/cfngin/hooks/docker/image/test_push.py
+++ b/tests/unit/cfngin/hooks/docker/image/test__push.py
@@ -1,6 +1,6 @@
 """Test runway.cfngin.hooks.docker.image._push."""
 
-# pyright: basic, reportFunctionMemberAccess=none
+# pyright: reportFunctionMemberAccess=none
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
@@ -33,7 +33,7 @@ def test_push(
 ) -> None:
     """Test push."""
     args = ImagePushArgs(repo="dkr.test.com/image", tags=["latest", "oldest"])
-    mocker.patch.object(ImagePushArgs, "parse_obj", return_value=args)
+    mocker.patch.object(ImagePushArgs, "model_validate", return_value=args)
     mocker.patch.object(DockerHookData, "client", mock_docker_client)
     docker_hook_data = DockerHookData()
     mock_from_cfngin_context = mocker.patch.object(
@@ -43,7 +43,7 @@ def test_push(
         DockerHookData, "update_context", return_value=docker_hook_data
     )
     cfngin_context.hook_data["docker"] = docker_hook_data
-    assert push(context=cfngin_context, **args.dict()) == docker_hook_data
+    assert push(context=cfngin_context, **args.model_dump()) == docker_hook_data
     mock_from_cfngin_context.assert_called_once_with(cfngin_context)
     docker_hook_data.client.api.push.assert_has_calls(
         [call(args.repo, tag=args.tags[0]), call(args.repo, tag=args.tags[1])]
@@ -62,7 +62,7 @@ def test__set_ecr_repo_from_dict(self) -> None:
             "registry_alias": "bar",
             "aws_region": "us-west-2",
         }
-        obj = ImagePushArgs.parse_obj({"ecr_repo": args})
+        obj = ImagePushArgs.model_validate({"ecr_repo": args})
         assert obj.ecr_repo
         assert obj.ecr_repo.name == args["repo_name"]
         assert obj.ecr_repo.registry.account_id == args["account_id"]
@@ -77,7 +77,7 @@ def test__set_repo(self) -> None:
     def test__set_repo_ecr(self) -> None:
         """Test _set_repo ECR."""
         repo = ElasticContainerRegistryRepository(
-            repo_name="test",
+            name="test",
             registry=ElasticContainerRegistry(account_id="123456789012", aws_region="us-east-1"),
         )
         assert ImagePushArgs(ecr_repo=repo).repo == repo.fqn
diff --git a/tests/unit/cfngin/hooks/docker/image/test_remove.py b/tests/unit/cfngin/hooks/docker/image/test__remove.py
similarity index 93%
rename from tests/unit/cfngin/hooks/docker/image/test_remove.py
rename to tests/unit/cfngin/hooks/docker/image/test__remove.py
index 3110cbc7e..74428ccbb 100644
--- a/tests/unit/cfngin/hooks/docker/image/test_remove.py
+++ b/tests/unit/cfngin/hooks/docker/image/test__remove.py
@@ -1,6 +1,5 @@
 """Test runway.cfngin.hooks.docker.image._remove."""
 
-# pyright: basic, reportFunctionMemberAccess=none
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
@@ -37,7 +36,7 @@ def test_remove(
     tags = ["latest", "oldest"]
     image = DockerImage(image=Image({"RepoTags": [f"{repo}:{tag}" for tag in tags]}))
     args = ImageRemoveArgs(force=True, image=image, tags=["latest", "oldest"])
-    mocker.patch.object(ImageRemoveArgs, "parse_obj", return_value=args)
+    mocker.patch.object(ImageRemoveArgs, "model_validate", return_value=args)
     mocker.patch.object(DockerHookData, "client", mock_docker_client)
     docker_hook_data = DockerHookData()
     docker_hook_data.image = image
@@ -67,7 +66,7 @@ def test_remove_image_not_found(
 ) -> None:
     """Test remove ImageNotFound."""
     args = ImageRemoveArgs(repo="dkr.test.com/image", tags=["latest"])
-    mocker.patch.object(ImageRemoveArgs, "parse_obj", return_value=args)
+    mocker.patch.object(ImageRemoveArgs, "model_validate", return_value=args)
     mocker.patch.object(DockerHookData, "client", mock_docker_client)
     docker_hook_data = DockerHookData()
     mocker.patch.object(DockerHookData, "from_cfngin_context", return_value=docker_hook_data)
@@ -78,7 +77,7 @@ def test_remove_image_not_found(
     docker_hook_data.client.api.remove_image.side_effect = ImageNotFound(  # type: ignore
         f"{args.repo}:latest"
     )
-    assert remove(context=cfngin_context, **args.dict()) == docker_hook_data
+    assert remove(context=cfngin_context, **args.model_dump()) == docker_hook_data
     docker_hook_data.client.api.remove_image.assert_has_calls(  # type: ignore
         [call(force=False, image=f"{args.repo}:{tag}", noprune=False) for tag in args.tags]
     )
@@ -96,7 +95,7 @@ def test__set_ecr_repo_from_dict(self) -> None:
             "registry_alias": "bar",
             "aws_region": "us-west-2",
         }
-        obj = ImageRemoveArgs.parse_obj({"ecr_repo": args})
+        obj = ImageRemoveArgs.model_validate({"ecr_repo": args})
         assert obj.ecr_repo
         assert obj.ecr_repo.name == args["repo_name"]
         assert obj.ecr_repo.registry.account_id == args["account_id"]
@@ -111,7 +110,7 @@ def test__set_repo(self) -> None:
     def test__set_repo_ecr(self) -> None:
         """Test _set_repo ECR."""
         repo = ElasticContainerRegistryRepository(
-            repo_name="test",
+            name="test",
             registry=ElasticContainerRegistry(account_id="123456789012", aws_region="us-east-1"),
         )
         assert ImageRemoveArgs(ecr_repo=repo).repo == repo.fqn
diff --git a/tests/unit/cfngin/hooks/docker/test_login.py b/tests/unit/cfngin/hooks/docker/test__login.py
similarity index 78%
rename from tests/unit/cfngin/hooks/docker/test_login.py
rename to tests/unit/cfngin/hooks/docker/test__login.py
index 639d310ba..23ba5e2df 100644
--- a/tests/unit/cfngin/hooks/docker/test_login.py
+++ b/tests/unit/cfngin/hooks/docker/test__login.py
@@ -1,10 +1,9 @@
 """Test runway.cfngin.hooks.docker._login."""
 
-# pyright: basic
 from __future__ import annotations
 
 from copy import deepcopy
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -29,7 +28,7 @@ def test_login(
 ) -> None:
     """Test login."""
     args = LoginArgs(password="p@ssword", registry="dkr.test.com", username="test-user")
-    mocker.patch.object(LoginArgs, "parse_obj", return_value=args)
+    mocker.patch.object(LoginArgs, "model_validate", return_value=args)
     mock_login = mocker.patch.object(mock_docker_client, "login")
     mocker.patch.object(DockerHookData, "client", mock_docker_client)
     docker_hook_data = DockerHookData()
@@ -40,9 +39,9 @@ def test_login(
         DockerHookData, "update_context", return_value=docker_hook_data
     )
     cfngin_context.hook_data["docker"] = docker_hook_data
-    assert login(context=cfngin_context, **args.dict()) == docker_hook_data
+    assert login(context=cfngin_context, **args.model_dump()) == docker_hook_data
     mock_from_cfngin_context.assert_called_once_with(cfngin_context)
-    mock_login.assert_called_once_with(**args.dict())
+    mock_login.assert_called_once_with(**args.model_dump())
     mock_update_context.assert_called_once_with(cfngin_context)
 
 
@@ -52,14 +51,16 @@ class TestLoginArgs:
     def test__set_ecr(self, mocker: MockerFixture) -> None:
         """Test _set_ecr."""
         expected = ElasticContainerRegistry(alias="foobar")
-        mock_parse_obj = mocker.patch.object(
-            ElasticContainerRegistry, "parse_obj", return_value=expected
+        mock_model_validate = mocker.patch.object(
+            ElasticContainerRegistry, "model_validate", return_value=expected
         )
         assert (
-            LoginArgs.parse_obj({"ecr": expected.dict(), "password": "", "username": ""}).ecr
+            LoginArgs.model_validate(
+                {"ecr": expected.model_dump(), "password": "", "username": ""}
+            ).ecr
             == expected
         )
-        mock_parse_obj.assert_called_once_with({"context": None, **expected.dict()})
+        mock_model_validate.assert_called_once_with({"context": None, **expected.model_dump()})
 
     @pytest.mark.parametrize(
         "ecr, registry, expected",
@@ -76,9 +77,9 @@ def test__set_ecr(self, mocker: MockerFixture) -> None:
     )
     def test__set_registry(
         self,
-        ecr: Optional[ElasticContainerRegistry],
-        expected: Optional[str],
-        registry: Optional[str],
+        ecr: ElasticContainerRegistry | None,
+        expected: str | None,
+        registry: str | None,
     ) -> None:
         """Test _set_registry."""
         assert LoginArgs(ecr=ecr, password="", registry=registry, username="").registry == expected
@@ -86,7 +87,7 @@ def test__set_registry(
     def test_field_defaults(self) -> None:
         """Test field defaults."""
         args = {"password": "p@ssword", "username": "test-user"}
-        obj = LoginArgs.parse_obj(deepcopy(args))
+        obj = LoginArgs.model_validate(deepcopy(args))
         assert not obj.dockercfg_path
         assert not obj.ecr
         assert not obj.email
diff --git a/tests/unit/cfngin/hooks/docker/test_data_models.py b/tests/unit/cfngin/hooks/docker/test_data_models.py
index 3d802dbd8..1ed2fc731 100644
--- a/tests/unit/cfngin/hooks/docker/test_data_models.py
+++ b/tests/unit/cfngin/hooks/docker/test_data_models.py
@@ -1,6 +1,5 @@
 """Test runway.cfngin.hooks.docker.data_models."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
@@ -92,7 +91,7 @@ def test_init_default(self, cfngin_context: MockCfnginContext) -> None:
         )
 
         with sts_stubber:
-            obj = ElasticContainerRegistry(context=cfngin_context)
+            obj = ElasticContainerRegistry.model_validate({"context": cfngin_context})
         sts_stubber.assert_no_pending_responses()
         assert obj.account_id == account_id
         assert obj.alias is None
@@ -101,12 +100,8 @@ def test_init_default(self, cfngin_context: MockCfnginContext) -> None:
 
     def test_init_no_context(self) -> None:
         """Test init with no context."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="context is required to resolve values"):
             ElasticContainerRegistry()
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("__root__",)
-        assert errors[0]["msg"] == "context is required to resolve values"
 
     def test_init_private(self) -> None:
         """Test init private."""
@@ -136,9 +131,9 @@ def test_fqn(self, cfngin_context: MockCfnginContext) -> None:
         region = "us-east-1"
 
         obj = ElasticContainerRegistryRepository(
-            repo_name="something",
-            registry=ElasticContainerRegistry(
-                account_id=account_id, aws_region=region, context=cfngin_context
+            name="something",
+            registry=ElasticContainerRegistry.model_validate(
+                {"account_id": account_id, "aws_region": region, "context": cfngin_context}
             ),
         )
         assert obj.fqn == f"{obj.registry.fqn}{obj.name}"
diff --git a/tests/unit/cfngin/hooks/docker/test_hook_data.py b/tests/unit/cfngin/hooks/docker/test_hook_data.py
index 605d5029c..49e3613c3 100644
--- a/tests/unit/cfngin/hooks/docker/test_hook_data.py
+++ b/tests/unit/cfngin/hooks/docker/test_hook_data.py
@@ -1,6 +1,5 @@
 """Test runway.cfngin.hooks.docker.hook_data."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/cfngin/hooks/ecr/test__purge_repositroy.py b/tests/unit/cfngin/hooks/ecr/test__purge_repositroy.py
index 28c10d828..97e4be9f4 100644
--- a/tests/unit/cfngin/hooks/ecr/test__purge_repositroy.py
+++ b/tests/unit/cfngin/hooks/ecr/test__purge_repositroy.py
@@ -1,6 +1,5 @@
 """Test runway.cfngin.hooks.ecr._purge_repository."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/cfngin/hooks/ssm/test_parameter.py b/tests/unit/cfngin/hooks/ssm/test_parameter.py
index 04a1f3535..d2504e280 100644
--- a/tests/unit/cfngin/hooks/ssm/test_parameter.py
+++ b/tests/unit/cfngin/hooks/ssm/test_parameter.py
@@ -45,8 +45,8 @@ def test_field_defaults(self) -> None:
 
     def test_name_required(self) -> None:
         """Test name."""
-        with pytest.raises(ValidationError, match="Name\n  field required"):
-            ArgsDataModel.parse_obj({"type": "String"})
+        with pytest.raises(ValidationError, match="Name\n  Field required"):
+            ArgsDataModel.model_validate({"type": "String"})
 
     def test_policies_raise_type_error(self) -> None:
         """Test policies."""
@@ -95,23 +95,29 @@ def test_tags_dict(self) -> None:
 
     def test_tags_raise_type_error(self) -> None:
         """Test tags."""
-        with pytest.raises(ValidationError, match="Tags"):
-            assert not ArgsDataModel.parse_obj({"name": "test", "tags": "", "type": "String"})
+        with pytest.raises(ValidationError, match="tags\n  Value error, unexpected type"):
+            assert not ArgsDataModel.model_validate({"name": "test", "tags": "", "type": "String"})
 
     def test_tier_invalid(self) -> None:
         """Test tier."""
-        with pytest.raises(ValidationError, match="Tier\n  unexpected value"):
-            ArgsDataModel.parse_obj({"name": "test", "tier": "invalid", "type": "String"})
+        with pytest.raises(
+            ValidationError,
+            match="tier\n  Input should be 'Advanced', 'Intelligent-Tiering' or 'Standard'",
+        ):
+            ArgsDataModel.model_validate({"name": "test", "tier": "invalid", "type": "String"})
 
     def test_type_invalid(self) -> None:
         """Test name."""
-        with pytest.raises(ValidationError, match="Type\n  unexpected value"):
-            ArgsDataModel.parse_obj({"name": "test", "type": "invalid"})
+        with pytest.raises(
+            ValidationError,
+            match="type\n  Input should be 'String', 'StringList' or 'SecureString'",
+        ):
+            ArgsDataModel.model_validate({"name": "test", "type": "invalid"})
 
     def test_type_required(self) -> None:
         """Test name."""
-        with pytest.raises(ValidationError, match="Type\n  field required"):
-            ArgsDataModel.parse_obj({"name": "test"})
+        with pytest.raises(ValidationError, match="Type\n  Field required"):
+            ArgsDataModel.model_validate({"name": "test"})
 
 
 class TestParameter:
@@ -120,12 +126,12 @@ class TestParameter:
     def test___init__(self, cfngin_context: CfnginContext, mocker: MockerFixture) -> None:
         """Test __init__."""
         args = mocker.patch(f"{MODULE}.ArgsDataModel")
-        args.parse_obj.return_value = args
+        args.model_validate.return_value = args
         data = {"key": "val"}
         obj = Parameter(cfngin_context, name="test", type="String", **data)
         assert obj.args == args
         assert obj.ctx == cfngin_context
-        args.parse_obj.assert_called_once_with({"name": "test", "type": "String", **data})
+        args.model_validate.assert_called_once_with({"name": "test", "type": "String", **data})
 
     def test_client(
         self,
diff --git a/tests/unit/cfngin/hooks/staticsite/test_upload_staticsite.py b/tests/unit/cfngin/hooks/staticsite/test_upload_staticsite.py
index 1e9a1a9d9..118eb93f7 100644
--- a/tests/unit/cfngin/hooks/staticsite/test_upload_staticsite.py
+++ b/tests/unit/cfngin/hooks/staticsite/test_upload_staticsite.py
@@ -1,10 +1,9 @@
 """Test runway.cfngin.hooks.staticsite.upload_staticsite."""
 
-# pyright: basic
 from __future__ import annotations
 
 import json
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 import pytest
 import yaml
@@ -17,7 +16,7 @@
     get_content_type,
     sync_extra_files,
 )
-from runway.module.staticsite.options.models import RunwayStaticSiteExtraFileDataModel
+from runway.module.staticsite.options import RunwayStaticSiteExtraFileDataModel
 
 if TYPE_CHECKING:
     from ....factories import MockCfnginContext
@@ -35,7 +34,7 @@
         (".test", None),
     ],
 )
-def test_auto_detect_content_type(provided: str, expected: Optional[str]) -> None:
+def test_auto_detect_content_type(provided: str, expected: str | None) -> None:
     """Test auto_detect_content_type."""
     assert auto_detect_content_type(provided) == expected
 
@@ -44,26 +43,26 @@ def test_auto_detect_content_type(provided: str, expected: Optional[str]) -> Non
     "provided, expected",
     [
         (
-            RunwayStaticSiteExtraFileDataModel.construct(
+            RunwayStaticSiteExtraFileDataModel.model_construct(
                 content_type="text/plain", name="test.txt"
             ),
             "text/plain",
         ),
         (
-            RunwayStaticSiteExtraFileDataModel.construct(
+            RunwayStaticSiteExtraFileDataModel.model_construct(
                 name="test.txt", content_type="text/plain"
             ),
             "text/plain",
         ),
         (
-            RunwayStaticSiteExtraFileDataModel.construct(name="test.json"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="test.json"),
             "application/json",
         ),
-        (RunwayStaticSiteExtraFileDataModel.construct(name="test.txt"), None),
+        (RunwayStaticSiteExtraFileDataModel.model_construct(name="test.txt"), None),
     ],
 )
 def test_get_content_type(
-    provided: RunwayStaticSiteExtraFileDataModel, expected: Optional[str]
+    provided: RunwayStaticSiteExtraFileDataModel, expected: str | None
 ) -> None:
     """Test get_content_type."""
     assert get_content_type(provided) == expected
@@ -111,16 +110,16 @@ def test_get_content_unsupported() -> None:
     "a, b",
     [
         (
-            RunwayStaticSiteExtraFileDataModel.construct(name="a"),
-            RunwayStaticSiteExtraFileDataModel.construct(name="b"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="a"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="b"),
         ),
         (
-            RunwayStaticSiteExtraFileDataModel.construct(name="test", content_type="a"),
-            RunwayStaticSiteExtraFileDataModel.construct(name="test", content_type="b"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="test", content_type="a"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="test", content_type="b"),
         ),
         (
-            RunwayStaticSiteExtraFileDataModel.construct(name="test", content="a"),
-            RunwayStaticSiteExtraFileDataModel.construct(name="test", content="b"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="test", content="a"),
+            RunwayStaticSiteExtraFileDataModel.model_construct(name="test", content="b"),
         ),
     ],
 )
@@ -172,7 +171,7 @@ def test_sync_extra_files_yaml_content(cfngin_context: MockCfnginContext) -> Non
         },
     )
 
-    files = [RunwayStaticSiteExtraFileDataModel.construct(name="test.yaml", content=content)]
+    files = [RunwayStaticSiteExtraFileDataModel.model_construct(name="test.yaml", content=content)]
 
     with s3_stub as stub:
         assert sync_extra_files(cfngin_context, "bucket", extra_files=files) == ["test.yaml"]
@@ -188,7 +187,7 @@ def test_sync_extra_files_empty_content(cfngin_context: MockCfnginContext) -> No
             cfngin_context,
             "bucket",
             extra_files=[
-                RunwayStaticSiteExtraFileDataModel.construct(name="test.yaml", content="")
+                RunwayStaticSiteExtraFileDataModel.model_construct(name="test.yaml", content="")
             ],
         )
         assert isinstance(result, list)
@@ -214,7 +213,7 @@ def test_sync_extra_files_file_reference(cfngin_context: MockCfnginContext) -> N
         },
     )
 
-    files = [RunwayStaticSiteExtraFileDataModel.construct(name="test", file=".gitignore")]
+    files = [RunwayStaticSiteExtraFileDataModel.model_construct(name="test", file=".gitignore")]
 
     with s3_stub as stub:
         assert sync_extra_files(cfngin_context, "bucket", extra_files=files) == ["test"]
@@ -238,7 +237,9 @@ def test_sync_extra_files_file_reference_with_content_type(
         },
     )
 
-    files = [RunwayStaticSiteExtraFileDataModel.construct(name="test.json", file=".gitignore")]
+    files = [
+        RunwayStaticSiteExtraFileDataModel.model_construct(name="test.json", file=".gitignore")
+    ]
 
     with s3_stub as stub:
         assert sync_extra_files(cfngin_context, "bucket", extra_files=files) == ["test.json"]
@@ -250,7 +251,7 @@ def test_sync_extra_files_hash_unchanged(cfngin_context: MockCfnginContext) -> N
     s3_stub = cfngin_context.add_stubber("s3")
     ssm_stub = cfngin_context.add_stubber("ssm")
 
-    extra = RunwayStaticSiteExtraFileDataModel.construct(name="test", content="test")
+    extra = RunwayStaticSiteExtraFileDataModel.model_construct(name="test", content="test")
     extra_hash = calculate_hash_of_extra_files([extra])
 
     ssm_stub.add_response(
diff --git a/tests/unit/cfngin/hooks/staticsite/test_utils.py b/tests/unit/cfngin/hooks/staticsite/test_utils.py
index c8829be4a..d88ca7651 100644
--- a/tests/unit/cfngin/hooks/staticsite/test_utils.py
+++ b/tests/unit/cfngin/hooks/staticsite/test_utils.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Optional, Union, cast
+from typing import TYPE_CHECKING, cast
 from unittest.mock import Mock, call
 
 import igittigitt
@@ -39,7 +39,7 @@ def test_calculate_hash_of_files(mocker: MockerFixture, tmp_path: Path) -> None:
     "directories", [None, [{"path": "./"}], [{"path": "./", "exclusions": ["foobar"]}]]
 )
 def test_get_hash_of_files(
-    directories: Optional[list[dict[str, Union[list[str], str]]]],
+    directories: list[dict[str, list[str] | str | None]] | None,
     mocker: MockerFixture,
     tmp_path: Path,
 ) -> None:
@@ -75,7 +75,7 @@ def test_get_hash_of_files(
     else:
         assert get_hash_of_files(tmp_path) == mock_calculate_hash_of_files.return_value
     mock_get_ignorer.assert_has_calls(
-        [  # type: ignore
+        [
             call(tmp_path / cast(str, i["path"]), i.get("exclusions"))
             for i in (directories or [{"path": "./"}])
         ]
@@ -85,7 +85,7 @@ def test_get_hash_of_files(
 
 @pytest.mark.parametrize("additional_exclusions", [None, [], ["foo"], ["foo", "bar"]])
 def test_get_ignorer(
-    additional_exclusions: Optional[list[str]], mocker: MockerFixture, tmp_path: Path
+    additional_exclusions: list[str] | None, mocker: MockerFixture, tmp_path: Path
 ) -> None:
     """Test get_ignorer."""
     ignore_parser = mocker.patch(f"{MODULE}.igittigitt.IgnoreParser")
diff --git a/tests/unit/cfngin/hooks/test_aws_lambda.py b/tests/unit/cfngin/hooks/test_awslambda.py
similarity index 97%
rename from tests/unit/cfngin/hooks/test_aws_lambda.py
rename to tests/unit/cfngin/hooks/test_awslambda.py
index 272e1c677..66ae10032 100644
--- a/tests/unit/cfngin/hooks/test_aws_lambda.py
+++ b/tests/unit/cfngin/hooks/test_awslambda.py
@@ -1,6 +1,6 @@
 """Tests for runway.cfngin.hooks.aws_lambda."""
 
-# pyright: basic, reportUnknownArgumentType=none, reportUnknownVariableType=none
+# pyright: reportUnknownArgumentType=none, reportUnknownVariableType=none
 # pyright: reportFunctionMemberAccess=none, reportOptionalMemberAccess=none
 # pyright: reportOptionalOperand=none
 from __future__ import annotations
@@ -14,7 +14,7 @@
 import unittest
 from io import BytesIO as StringIO
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import ANY, MagicMock, patch
 from zipfile import ZipFile
 
@@ -71,7 +71,7 @@ class TestLambdaHooks(unittest.TestCase):
 
     @classmethod
     def temp_directory_with_files(
-        cls, files: Union[list[str], tuple[str, ...]] = ALL_FILES
+        cls, files: list[str] | tuple[str, ...] = ALL_FILES
     ) -> TempDirectory:
         """Create a temp directory with files."""
         temp_dict = TempDirectory()
@@ -412,7 +412,7 @@ def test_calculate_hash_different_ordering(self) -> None:
 
     def test_select_bucket_region(self) -> None:
         """Test select bucket region."""
-        tests: tuple[tuple[tuple[Optional[str], Optional[str], Optional[str], str], str], ...] = (
+        tests: tuple[tuple[tuple[str | None, str | None, str | None, str], str], ...] = (
             (("myBucket", "us-east-1", "us-west-1", "eu-west-1"), "us-east-1"),
             (("myBucket", None, "us-west-1", "eu-west-1"), "eu-west-1"),
             ((None, "us-east-1", "us-west-1", "eu-west-1"), "us-west-1"),
@@ -420,7 +420,7 @@ def test_select_bucket_region(self) -> None:
         )
 
         for args, result in tests:
-            assert select_bucket_region(*args) == result  # type: ignore
+            assert select_bucket_region(*args) == result
 
     @mock_aws
     def test_follow_symlink_nonbool(self) -> None:
@@ -753,7 +753,7 @@ def test_explicit_pipenv(self, tmp_path: Path) -> None:
     def test_frozen_pipenv(
         self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
     ) -> None:
-        """Test use pipenv from Pyinstaller build."""
+        """Test use pipenv from Pyinstaller build."""  # cspell:ignore Pyinstaller
         caplog.set_level(logging.ERROR, logger="runway.cfngin.hooks.aws_lambda")
         monkeypatch.setattr("runway.cfngin.hooks.aws_lambda.sys.frozen", True, raising=False)
 
@@ -854,14 +854,14 @@ def test_copydir() -> None:
     with TempDirectory() as tmp_dir:
         dest_path = tmp_dir.makedir("dest")
         src_path = tmp_dir.makedir("src")
-        tmp_dir.makedir(("src", "lib"))
+        tmp_dir.makedir("src/lib")
         example_file = b"example file content"
-        tmp_dir.write(("src", "example_file"), example_file)
-        tmp_dir.write(("src", "lib", "example_file"), example_file)
+        tmp_dir.write("src/example_file", example_file)
+        tmp_dir.write("src/lib/example_file", example_file)
 
         copydir(src_path, dest_path, ["**"])
 
-        assert tmp_dir.read(("src", "example_file")) == example_file
-        assert tmp_dir.read(("src", "lib", "example_file")) == example_file
-        assert tmp_dir.read(("dest", "example_file")) == example_file
-        assert tmp_dir.read(("dest", "lib", "example_file")) == example_file
+        assert tmp_dir.read("src/example_file") == example_file
+        assert tmp_dir.read("src/lib/example_file") == example_file
+        assert tmp_dir.read("dest/example_file") == example_file
+        assert tmp_dir.read("dest/lib/example_file") == example_file
diff --git a/tests/unit/cfngin/hooks/test_base.py b/tests/unit/cfngin/hooks/test_base.py
index f4047077d..2b9d0df14 100644
--- a/tests/unit/cfngin/hooks/test_base.py
+++ b/tests/unit/cfngin/hooks/test_base.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.hooks.base."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/cfngin/hooks/test_command.py b/tests/unit/cfngin/hooks/test_command.py
index 9d2d9aee4..56977beed 100644
--- a/tests/unit/cfngin/hooks/test_command.py
+++ b/tests/unit/cfngin/hooks/test_command.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.hooks.command."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/cfngin/hooks/test_iam.py b/tests/unit/cfngin/hooks/test_iam.py
index 666cca6f1..150096090 100644
--- a/tests/unit/cfngin/hooks/test_iam.py
+++ b/tests/unit/cfngin/hooks/test_iam.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.hooks.iam."""
 
-# pyright: basic
 from __future__ import annotations
 
 from datetime import datetime
diff --git a/tests/unit/cfngin/hooks/test_keypair.py b/tests/unit/cfngin/hooks/test_keypair.py
index e0dd79db7..13aa19b8c 100644
--- a/tests/unit/cfngin/hooks/test_keypair.py
+++ b/tests/unit/cfngin/hooks/test_keypair.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.hooks.keypair."""
 
-# pyright: basic
 from __future__ import annotations
 
 import os
diff --git a/tests/unit/cfngin/lookups/handlers/test_ami.py b/tests/unit/cfngin/lookups/handlers/test_ami.py
index 86bc853da..567fe6816 100644
--- a/tests/unit/cfngin/lookups/handlers/test_ami.py
+++ b/tests/unit/cfngin/lookups/handlers/test_ami.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.lookups.handlers.ami."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/cfngin/lookups/handlers/test_awslambda.py b/tests/unit/cfngin/lookups/handlers/test_awslambda.py
index 4044271d3..c38bc8501 100644
--- a/tests/unit/cfngin/lookups/handlers/test_awslambda.py
+++ b/tests/unit/cfngin/lookups/handlers/test_awslambda.py
@@ -45,7 +45,7 @@ def test_get_deployment_package_data(self, hook_data: AwsLambdaHookDeployRespons
         data_key = "test.key"
         assert (
             AwsLambdaLookup.get_deployment_package_data(
-                Mock(hook_data={data_key: hook_data.dict(by_alias=True)}),
+                Mock(hook_data={data_key: hook_data.model_dump(by_alias=True)}),
                 data_key,
             )
             == hook_data
@@ -59,7 +59,7 @@ def test_get_deployment_package_data_set_hook_data(
     ) -> None:
         """Test get_deployment_package_data set hook_data when it's missing."""
         data_key = "test.key"
-        hook = Mock(plan=Mock(return_value=hook_data.dict(by_alias=True)))
+        hook = Mock(plan=Mock(return_value=hook_data.model_dump(by_alias=True)))
         init_hook_class = mocker.patch.object(AwsLambdaLookup, "init_hook_class", return_value=hook)
         get_hook_definition = mocker.patch.object(
             AwsLambdaLookup, "get_required_hook_definition", return_value="hook_def"
@@ -68,7 +68,7 @@ def test_get_deployment_package_data_set_hook_data(
         get_hook_definition.assert_called_once_with(cfngin_context.config, data_key)
         init_hook_class.assert_called_once_with(cfngin_context, get_hook_definition.return_value)
         hook.plan.assert_called_once_with()
-        assert cfngin_context.hook_data[data_key] == hook_data.dict(by_alias=True)
+        assert cfngin_context.hook_data[data_key] == hook_data.model_dump(by_alias=True)
 
     def test_get_deployment_package_data_raise_type_error(self) -> None:
         """Test get_deployment_package_data."""
diff --git a/tests/unit/cfngin/lookups/handlers/test_file.py b/tests/unit/cfngin/lookups/handlers/test_file.py
index 79e798123..8ec9a3311 100644
--- a/tests/unit/cfngin/lookups/handlers/test_file.py
+++ b/tests/unit/cfngin/lookups/handlers/test_file.py
@@ -1,6 +1,6 @@
 """Tests for runway.cfngin.lookups.handlers.file."""
 
-# pyright: basic, reportUnknownArgumentType=none, reportUnknownVariableType=none
+# pyright: reportUnknownArgumentType=none, reportUnknownVariableType=none
 from __future__ import annotations
 
 import base64
@@ -39,15 +39,11 @@ class TestArgsDataModel:
 
     def test__validate_supported_codec_raise_value_error(self) -> None:
         """Test _validate_supported_codec raise ValueError."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match=rf".*Value error, Codec 'foo' must be one of: {', '.join(CODECS)}",
+        ):
             ArgsDataModel(codec="foo")
-        assert excinfo.value.errors() == [
-            {
-                "loc": ("codec",),
-                "msg": f"Codec 'foo' must be one of: {', '.join(CODECS)}",
-                "type": "value_error",
-            }
-        ]
 
 
 class TestFileLookup:
@@ -144,15 +140,11 @@ def test_handle_plain(self, tmp_path: Path) -> None:
 
     def test_handle_raise_validation_error(self) -> None:
         """Test handle raise ValidationError."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match=rf".*Value error, Codec 'foo' must be one of: {', '.join(CODECS)}",
+        ):
             FileLookup.handle("foo:bar")
-        assert excinfo.value.errors() == [
-            {
-                "loc": ("codec",),
-                "msg": f"Codec 'foo' must be one of: {', '.join(CODECS)}",
-                "type": "value_error",
-            }
-        ]
 
     def test_handle_raise_value_error(self) -> None:
         """Test handle raise ValueError."""
diff --git a/tests/unit/cfngin/lookups/handlers/test_hook_data.py b/tests/unit/cfngin/lookups/handlers/test_hook_data.py
index bd50703b9..b2fe1ffa4 100644
--- a/tests/unit/cfngin/lookups/handlers/test_hook_data.py
+++ b/tests/unit/cfngin/lookups/handlers/test_hook_data.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.lookups.handlers.hook_data."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/cfngin/lookups/handlers/test_kms.py b/tests/unit/cfngin/lookups/handlers/test_kms.py
index cbcfc06da..8106c2a09 100644
--- a/tests/unit/cfngin/lookups/handlers/test_kms.py
+++ b/tests/unit/cfngin/lookups/handlers/test_kms.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.lookups.handlers.kms."""
 
-# pyright: basic
 from __future__ import annotations
 
 import codecs
diff --git a/tests/unit/cfngin/lookups/handlers/test_xref.py b/tests/unit/cfngin/lookups/handlers/test_xref.py
index ec389d604..e1f4b2e57 100644
--- a/tests/unit/cfngin/lookups/handlers/test_xref.py
+++ b/tests/unit/cfngin/lookups/handlers/test_xref.py
@@ -1,6 +1,6 @@
 """Tests for runway.cfngin.lookups.handlers.xref."""
 
-# pyright: basic, reportUnknownArgumentType=none, reportUnknownVariableType=none
+# pyright: reportUnknownArgumentType=none, reportUnknownVariableType=none
 import unittest
 from unittest.mock import MagicMock
 
diff --git a/tests/unit/cfngin/lookups/test_registry.py b/tests/unit/cfngin/lookups/test_registry.py
index dd13b7501..ff57a145b 100644
--- a/tests/unit/cfngin/lookups/test_registry.py
+++ b/tests/unit/cfngin/lookups/test_registry.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.lookups.registry."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
diff --git a/tests/unit/cfngin/providers/aws/test_default.py b/tests/unit/cfngin/providers/aws/test_default.py
index 118845425..d89232b38 100644
--- a/tests/unit/cfngin/providers/aws/test_default.py
+++ b/tests/unit/cfngin/providers/aws/test_default.py
@@ -11,7 +11,7 @@
 from contextlib import suppress
 from datetime import datetime
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, patch
 
 import boto3
@@ -63,7 +63,7 @@ def random_string(length: int = 12) -> str:
 
 def generate_describe_stacks_stack(
     stack_name: str,
-    creation_time: Optional[datetime] = None,
+    creation_time: datetime | None = None,
     stack_status: Literal[
         "CREATE_IN_PROGRESS",
         "CREATE_FAILED",
@@ -88,7 +88,7 @@ def generate_describe_stacks_stack(
         "IMPORT_ROLLBACK_FAILED",
         "IMPORT_ROLLBACK_COMPLETE",
     ] = "CREATE_COMPLETE",
-    tags: Optional[TagSetTypeDef] = None,
+    tags: TagSetTypeDef | None = None,
     termination_protection: bool = False,
 ) -> StackTypeDef:
     """Generate describe stacks stack."""
@@ -113,7 +113,7 @@ def generate_get_template(
     }
 
 
-def generate_stack_object(stack_name: str, outputs: Optional[dict[str, Any]] = None) -> MagicMock:
+def generate_stack_object(stack_name: str, outputs: dict[str, Any] | None = None) -> MagicMock:
     """Generate stack object."""
     mock_stack = MagicMock(["name", "fqn", "blueprint"])
     if not outputs:
@@ -145,7 +145,7 @@ def generate_resource_change(replacement: bool = True) -> ChangeTypeDef:
 def generate_change_set_response(
     status: str,
     execution_status: str = "AVAILABLE",
-    changes: Optional[list[dict[str, Any]]] = None,
+    changes: list[dict[str, Any]] | None = None,
     status_reason: str = "FAKE",
 ) -> dict[str, Any]:
     """Generate change set response."""
diff --git a/tests/unit/cfngin/test_dag.py b/tests/unit/cfngin/test_dag.py
index 09b146854..aea1e8103 100644
--- a/tests/unit/cfngin/test_dag.py
+++ b/tests/unit/cfngin/test_dag.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.dag."""
 
-# pyright: basic
 import threading
 from typing import Any
 
diff --git a/tests/unit/cfngin/test_exceptions.py b/tests/unit/cfngin/test_exceptions.py
index 6a79f25c4..092ee8b01 100644
--- a/tests/unit/cfngin/test_exceptions.py
+++ b/tests/unit/cfngin/test_exceptions.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -29,9 +29,7 @@ class TestCfnginBucketRequired:
             (Path("/tmp"), "something", f"; something ({Path('/tmp')})"),
         ],
     )
-    def test___init__(
-        self, config_path: Optional[AnyPath], reason: Optional[str], expected: str
-    ) -> None:
+    def test___init__(self, config_path: AnyPath | None, reason: str | None, expected: str) -> None:
         """Test __init__."""
         expected_msg = f"cfngin_bucket is required{expected}"
         obj = CfnginBucketRequired(config_path=config_path, reason=reason)
@@ -50,9 +48,7 @@ class TestInvalidConfig:
         "errors, expected_msg",
         [("error", "error"), (["error0", "error1"], "error0\nerror1")],
     )
-    def test___init__(
-        self, errors: Union[str, list[Union[Exception, str]]], expected_msg: str
-    ) -> None:
+    def test___init__(self, errors: str | list[Exception | str], expected_msg: str) -> None:
         """Test __init__."""
         obj = InvalidConfig(errors)
         assert obj.errors == errors
@@ -76,9 +72,7 @@ class TestPersistentGraphLocked:
             (None, "reason", "Persistent graph is locked. reason"),
         ],
     )
-    def test___init__(
-        self, message: Optional[str], reason: Optional[str], expected_msg: str
-    ) -> None:
+    def test___init__(self, message: str | None, reason: str | None, expected_msg: str) -> None:
         """Test __init__."""
         obj = PersistentGraphLocked(message=message, reason=reason)
         assert obj.message == expected_msg
@@ -101,9 +95,7 @@ class TestPersistentGraphUnlocked:
             (None, "reason", "Persistent graph is unlocked. reason"),
         ],
     )
-    def test___init__(
-        self, message: Optional[str], reason: Optional[str], expected_msg: str
-    ) -> None:
+    def test___init__(self, message: str | None, reason: str | None, expected_msg: str) -> None:
         """Test __init__."""
         obj = PersistentGraphUnlocked(message=message, reason=reason)
         assert obj.message == expected_msg
diff --git a/tests/unit/cfngin/test_tokenize_userdata.py b/tests/unit/cfngin/test_tokenize_userdata.py
index ec08288cd..24696074f 100644
--- a/tests/unit/cfngin/test_tokenize_userdata.py
+++ b/tests/unit/cfngin/test_tokenize_userdata.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.tokenize_userdata."""
 
-# pyright: basic
 import unittest
 
 import yaml
diff --git a/tests/unit/cfngin/test_utils.py b/tests/unit/cfngin/test_utils.py
index 7863e832e..89ed3c1ed 100644
--- a/tests/unit/cfngin/test_utils.py
+++ b/tests/unit/cfngin/test_utils.py
@@ -1,6 +1,5 @@
 """Tests for runway.cfngin.utils."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/config/components/runway/test_deployment_def.py b/tests/unit/config/components/runway/test__deployment_def.py
similarity index 92%
rename from tests/unit/config/components/runway/test_deployment_def.py
rename to tests/unit/config/components/runway/test__deployment_def.py
index 077a36956..c1b6b5a42 100644
--- a/tests/unit/config/components/runway/test_deployment_def.py
+++ b/tests/unit/config/components/runway/test__deployment_def.py
@@ -1,6 +1,5 @@
 """Test runway.config.components.runway._deployment_dev."""
 
-# pyright: basic
 from typing import Any
 
 import pytest
@@ -93,8 +92,8 @@ def test_modules_setter(self) -> None:
             RunwayModuleDefinition.parse_obj({"name": "test-02", "path": "./"}),
         ]
         obj.modules = new_modules
-        assert obj._data.modules[0] == new_modules[0].data
-        assert obj._data.modules[1] == new_modules[1].data
+        assert obj._data.modules[0].model_dump() == new_modules[0].data
+        assert obj._data.modules[1].model_dump() == new_modules[1].data
 
     def test_modules_setter_not_list(self) -> None:
         """Test modules.setter not a list."""
@@ -106,8 +105,8 @@ def test_modules_setter_not_list(self) -> None:
         with pytest.raises(TypeError):
             obj.modules = None  # type: ignore
         with pytest.raises(TypeError):
-            obj.modules = [  # type: ignore
-                RunwayDeploymentDefinitionModel(modules=[], name="test-01", regions=["us-east-1"])
+            obj.modules = [
+                RunwayDeploymentDefinitionModel(modules=[], name="test-01", regions=["us-east-1"])  # type: ignore
             ]
 
     def test_models_setter_invalid_list_item(self) -> None:
@@ -120,7 +119,7 @@ def test_parse_obj(self) -> None:
         """Test parse_obj."""
         data: dict[str, Any] = {"name": "test", "modules": [], "regions": ["us-east-1"]}
         obj = RunwayDeploymentDefinition.parse_obj(data)
-        assert obj._data.dict(exclude_unset=True) == data
+        assert obj._data.model_dump(exclude_unset=True) == data
 
     def test_parse_obj_list(self) -> None:
         """Test parse_obj list."""
@@ -129,7 +128,7 @@ def test_parse_obj_list(self) -> None:
 
         assert isinstance(result, list)
         assert len(result) == 1
-        assert result[0]._data.dict(exclude_unset=True) == data[0]
+        assert result[0]._data.model_dump(exclude_unset=True) == data[0]
 
     def test_register_variable(self) -> None:
         """Test _register_variable."""
@@ -138,7 +137,7 @@ def test_register_variable(self) -> None:
 
     def test_reverse(self) -> None:
         """Test reverse."""
-        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.parse_obj(
+        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.model_validate(
             {
                 "name": "test",
                 "modules": [
@@ -157,7 +156,7 @@ def test_reverse(self) -> None:
 
     def test_reverse_parallel_modules(self) -> None:
         """Test reverse parallel modules."""
-        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.parse_obj(
+        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.model_validate(
             {
                 "name": "test",
                 "modules": [
@@ -174,14 +173,14 @@ def test_reverse_parallel_modules(self) -> None:
         obj = RunwayDeploymentDefinition(data)
         assert not obj.reverse()
         assert obj._data.modules != data.modules
-        invert_data: RunwayDeploymentDefinitionModel = data.copy(deep=True)
+        invert_data: RunwayDeploymentDefinitionModel = data.model_copy(deep=True)
         for mod in invert_data.modules:
             mod.parallel.reverse()
         assert obj._data.modules == invert_data.modules
 
     def test_reverse_parallel_regions(self) -> None:
         """Test reverse parallel regions."""
-        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.parse_obj(
+        data: RunwayDeploymentDefinitionModel = RunwayDeploymentDefinitionModel.model_validate(
             {
                 "name": "test",
                 "modules": [{"name": "test-01", "path": "./"}],
diff --git a/tests/unit/config/components/runway/test_module_def.py b/tests/unit/config/components/runway/test__module_def.py
similarity index 96%
rename from tests/unit/config/components/runway/test_module_def.py
rename to tests/unit/config/components/runway/test__module_def.py
index d6a98922f..4f47fadc8 100644
--- a/tests/unit/config/components/runway/test_module_def.py
+++ b/tests/unit/config/components/runway/test__module_def.py
@@ -1,6 +1,5 @@
 """Test runway.config.components.runway._module_def."""
 
-# pyright: basic
 from pathlib import Path
 from typing import Any
 
@@ -32,7 +31,7 @@ def test_child_modules_setter(self) -> None:
         ]
         obj.child_modules = new_modules
         assert obj._data.parallel[0] == new_modules[0]
-        assert obj._data.parallel[1] == new_modules[1].data  # type: ignore
+        assert obj._data.parallel[1].model_dump() == new_modules[1].data  # type: ignore
 
     def test_child_modules_setter_not_list(self) -> None:
         """Test child_modules.setter not a list."""
@@ -122,7 +121,7 @@ def test_parse_obj(self) -> None:
         """Test parse_obj."""
         data = {"name": Path.cwd().name, "path": "./"}
         obj = RunwayModuleDefinition.parse_obj(data)
-        assert obj._data.dict(exclude_unset=True) == data
+        assert obj._data.model_dump(exclude_unset=True) == data
 
     def test_register_variable(self) -> None:
         """Test _register_variable."""
@@ -131,7 +130,7 @@ def test_register_variable(self) -> None:
 
     def test_reverse(self) -> None:
         """Test reverse."""
-        data: RunwayModuleDefinitionModel = RunwayModuleDefinitionModel.parse_obj(
+        data: RunwayModuleDefinitionModel = RunwayModuleDefinitionModel.model_validate(
             {
                 "name": "parallel_parent",
                 "parallel": [
diff --git a/tests/unit/config/components/runway/test__test_def.py b/tests/unit/config/components/runway/test__test_def.py
new file mode 100644
index 000000000..41eb172ee
--- /dev/null
+++ b/tests/unit/config/components/runway/test__test_def.py
@@ -0,0 +1,27 @@
+"""Test runway.config.components.runway._test_def."""
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.components.runway import RunwayTestDefinition
+
+
+class TestRunwayTestDefinition:
+    """Test runway.config.components.runway._test_def.RunwayTestDefinition."""
+
+    def test_new_invalid(self) -> None:
+        """Test new invalid type."""
+        with pytest.raises(ValidationError, match="Input should be a valid dictionary or instance"):
+            RunwayTestDefinition.parse_obj([])
+
+    def test_parse_obj_invalid(self) -> None:
+        """Test parse_obj invalid object."""
+        with pytest.raises(ValidationError):
+            RunwayTestDefinition.parse_obj({"type": "invalid"})
+
+    def test_register_variable(self) -> None:
+        """Test _register_variable."""
+        obj = RunwayTestDefinition.parse_obj(
+            {"type": "script", "name": "test_register_variable", "required": True}
+        )
+        assert obj._vars["required"].name == "test_register_variable.required"
diff --git a/tests/unit/config/components/runway/test_variables_def.py b/tests/unit/config/components/runway/test__variables_def.py
similarity index 99%
rename from tests/unit/config/components/runway/test_variables_def.py
rename to tests/unit/config/components/runway/test__variables_def.py
index 2b800a105..b51b7d9d8 100644
--- a/tests/unit/config/components/runway/test_variables_def.py
+++ b/tests/unit/config/components/runway/test__variables_def.py
@@ -1,6 +1,5 @@
 """Test runway.config.components.runway._variables_def."""
 
-# pyright: basic
 from pathlib import Path
 
 import pytest
diff --git a/tests/unit/config/components/runway/test_base.py b/tests/unit/config/components/runway/test_base.py
index 71836e5ff..c06e580f7 100644
--- a/tests/unit/config/components/runway/test_base.py
+++ b/tests/unit/config/components/runway/test_base.py
@@ -1,13 +1,12 @@
 """Test runway.config.components.runway.base."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, call
 
 import pytest
-from pydantic import Extra
+from pydantic import ConfigDict
 
 from runway.config.components.runway import RunwayVariablesDefinition
 from runway.config.components.runway.base import ConfigComponentDefinition
@@ -22,17 +21,12 @@
 class SampleConfigProperty(ConfigProperty):
     """Data class for SampleConfigComponentDefinition."""
 
+    model_config = ConfigDict(extra="allow", validate_assignment=True, validate_default=True)
+
     name: str = "test"
     var_attr: Any = None
     var_attr_pre: Any = None
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.allow
-        validate_all = False
-        validate_assignment = False
-
 
 class SampleConfigComponentDefinition(ConfigComponentDefinition):
     """Subclass used to test a parent class."""
@@ -72,7 +66,7 @@ def parse_obj(cls, obj: Any) -> SampleConfigComponentDefinition:
             obj: The object to parse.
 
         """
-        return cls(SampleConfigProperty.parse_obj(obj))
+        return cls(SampleConfigProperty.model_validate(obj))
 
 
 class TestConfigComponentDefinition:
@@ -93,7 +87,7 @@ def test_default(self) -> None:
         data = SampleConfigProperty()
         obj = SampleConfigComponentDefinition(data)
         assert obj._data == data
-        assert obj.data == data.dict()
+        assert obj.data == data.model_dump()
         assert not obj._vars
         assert isinstance(obj._vars, dict)
 
diff --git a/tests/unit/config/components/runway/test_test_def.py b/tests/unit/config/components/runway/test_test_def.py
deleted file mode 100644
index cffcb1ca1..000000000
--- a/tests/unit/config/components/runway/test_test_def.py
+++ /dev/null
@@ -1,105 +0,0 @@
-"""Test runway.config.components.runway._test_def."""
-
-# pyright: basic
-import pytest
-from pydantic import ValidationError
-
-from runway.config.components.runway import (
-    CfnLintRunwayTestDefinition,
-    RunwayTestDefinition,
-    ScriptRunwayTestDefinition,
-    YamlLintRunwayTestDefinition,
-)
-from runway.config.models.runway import (
-    CfnLintRunwayTestDefinitionModel,
-    ScriptRunwayTestDefinitionModel,
-    YamlLintRunwayTestDefinitionModel,
-)
-
-
-class TestCfnLintRunwayTestDefinition:
-    """Test runway.config.components.runway._test_def.CfnLintRunwayTestDefinition."""
-
-    def test_parse_obj(self) -> None:
-        """Test parse_obj."""
-        assert isinstance(CfnLintRunwayTestDefinition.parse_obj({}), CfnLintRunwayTestDefinition)
-
-
-class TestRunwayTestDefinition:
-    """Test runway.config.components.runway._test_def.RunwayTestDefinition."""
-
-    def test_new_cfn_lint(self) -> None:
-        """Test creation CfnLintRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition(CfnLintRunwayTestDefinitionModel()),
-            CfnLintRunwayTestDefinition,
-        )
-
-    def test_new_invalid(self) -> None:
-        """Test new invalid type."""
-        with pytest.raises(TypeError) as excinfo:
-            RunwayTestDefinition({})  # type: ignore
-        assert str(excinfo.value).startswith("expected data of type")
-
-    def test_new_script(self) -> None:
-        """Test creation ScriptRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition(ScriptRunwayTestDefinitionModel()),
-            ScriptRunwayTestDefinition,
-        )
-
-    def test_new_yamllint(self) -> None:
-        """Test creation ScriptRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition(YamlLintRunwayTestDefinitionModel()),
-            YamlLintRunwayTestDefinition,
-        )
-
-    def test_parse_obj_cfn_lint(self) -> None:
-        """Test parse_obj CfnLintRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition.parse_obj({"type": "cfn-lint"}),
-            CfnLintRunwayTestDefinition,
-        )
-
-    def test_parse_obj_invalid(self) -> None:
-        """Test parse_obj invalid object."""
-        with pytest.raises(ValidationError):
-            RunwayTestDefinition.parse_obj({"type": "invalid"})
-
-    def test_parse_obj_script(self) -> None:
-        """Test parse_obj ScriptRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition.parse_obj({"type": "script"}),
-            ScriptRunwayTestDefinition,
-        )
-
-    def test_parse_obj_yamllint(self) -> None:
-        """Test parse_obj YamlLintRunwayTestDefinition."""
-        assert isinstance(
-            RunwayTestDefinition.parse_obj({"type": "yamllint"}),
-            YamlLintRunwayTestDefinition,
-        )
-
-    def test_register_variable(self) -> None:
-        """Test _register_variable."""
-        obj = RunwayTestDefinition.parse_obj(
-            {"type": "script", "name": "test_register_variable", "required": True}
-        )
-        assert obj._vars["required"].name == "test_register_variable.required"
-
-
-class TestScriptRunwayTestDefinition:
-    """Test runway.config.components.runway._test_def.ScriptRunwayTestDefinition."""
-
-    def test_parse_obj(self) -> None:
-        """Test parse_obj."""
-        assert isinstance(ScriptRunwayTestDefinition.parse_obj({}), ScriptRunwayTestDefinition)
-
-
-class TestYamlLintRunwayTestDefinition:
-    """Test runway.config.components.runway._test_def.YamlLintRunwayTestDefinition."""
-
-    def test_parse_obj(self) -> None:
-        """Test parse_obj."""
-        assert isinstance(YamlLintRunwayTestDefinition.parse_obj({}), YamlLintRunwayTestDefinition)
diff --git a/tests/unit/config/models/cfngin/test_cfngin.py b/tests/unit/config/models/cfngin/test___init__.py
similarity index 72%
rename from tests/unit/config/models/cfngin/test_cfngin.py
rename to tests/unit/config/models/cfngin/test___init__.py
index b78e18930..d951fc9ad 100644
--- a/tests/unit/config/models/cfngin/test_cfngin.py
+++ b/tests/unit/config/models/cfngin/test___init__.py
@@ -1,6 +1,5 @@
 """Test runway.config.models.cfngin.__init__."""
 
-# pyright: basic
 import platform
 from pathlib import Path
 
@@ -25,15 +24,15 @@ def test_convert_hook_definitions(self, field: str) -> None:
         dict_hook = {"name": {"path": "something"}}
         list_hook = [{"path": "something"}]
         assert (
-            CfnginConfigDefinitionModel.parse_obj({"namespace": "test", field: dict_hook}).dict(
-                exclude_unset=True
-            )[field]
+            CfnginConfigDefinitionModel.model_validate(
+                {"namespace": "test", field: dict_hook}
+            ).model_dump(exclude_unset=True)[field]
             == list_hook
         )
         assert (
-            CfnginConfigDefinitionModel.parse_obj({"namespace": "test", field: list_hook}).dict(
-                exclude_unset=True
-            )[field]
+            CfnginConfigDefinitionModel.model_validate(
+                {"namespace": "test", field: list_hook}
+            ).model_dump(exclude_unset=True)[field]
             == list_hook
         )
 
@@ -45,18 +44,14 @@ def test_convert_stack_definitions(self) -> None:
             CfnginConfigDefinitionModel(
                 namespace="test",
                 stacks=dict_stack,  # type: ignore
-            ).dict(
-                exclude_unset=True
-            )["stacks"]
+            ).model_dump(exclude_unset=True)["stacks"]
             == list_stack
         )
         assert (
             CfnginConfigDefinitionModel(
                 namespace="test",
                 stacks=list_stack,  # type: ignore
-            ).dict(
-                exclude_unset=True
-            )["stacks"]
+            ).model_dump(exclude_unset=True)["stacks"]
             == list_stack
         )
 
@@ -119,12 +114,8 @@ def test_resolve_path_fields(self) -> None:
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginConfigDefinitionModel.parse_obj({})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("namespace",)
-        assert errors[0]["msg"] == "field required"
+        with pytest.raises(ValidationError, match="namespace\n  Field required"):
+            CfnginConfigDefinitionModel.model_validate({})
 
     def test_validate_unique_stack_names(self) -> None:
         """Test _validate_unique_stack_names."""
@@ -135,12 +126,14 @@ def test_validate_unique_stack_names(self) -> None:
                 {"name": "stack1", "class_path": "stack1"},
             ],
         }
-        assert CfnginConfigDefinitionModel.parse_obj(data)
+        assert CfnginConfigDefinitionModel.model_validate(data)
 
     def test_validate_unique_stack_names_invalid(self) -> None:
         """Test _validate_unique_stack_names."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginConfigDefinitionModel.parse_obj(
+        with pytest.raises(
+            ValidationError, match="stacks\n  Value error, Duplicate stack stack0 found at index 0"
+        ):
+            CfnginConfigDefinitionModel.model_validate(
                 {
                     "namespace": "test",
                     "stacks": [
@@ -149,10 +142,6 @@ def test_validate_unique_stack_names_invalid(self) -> None:
                     ],
                 }
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("stacks",)
-        assert errors[0]["msg"] == "Duplicate stack stack0 found at index 0"
 
 
 class TestCfnginHookDefinitionModel:
@@ -160,15 +149,11 @@ class TestCfnginHookDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             CfnginHookDefinitionModel(
                 invalid="something",  # type: ignore
                 path="something",
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
@@ -181,12 +166,8 @@ def test_field_defaults(self) -> None:
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginHookDefinitionModel.parse_obj({})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("path",)
-        assert errors[0]["msg"] == "field required"
+        with pytest.raises(ValidationError, match="path\n  Field required"):
+            CfnginHookDefinitionModel.model_validate({})
 
 
 class TestCfnginStackDefinitionModel:
@@ -194,16 +175,12 @@ class TestCfnginStackDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             CfnginStackDefinitionModel(
                 class_path="something",
                 invalid="something",  # type: ignore
                 name="stack-name",
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
@@ -227,12 +204,10 @@ def test_field_defaults(self) -> None:
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginStackDefinitionModel.parse_obj({})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("__root__",)
-        assert errors[0]["msg"] == "either class_path or template_path must be defined"
+        with pytest.raises(
+            ValidationError, match="Value error, either class_path or template_path must be defined"
+        ):
+            CfnginStackDefinitionModel.model_validate({})
 
     @pytest.mark.skipif(
         platform.system() == "Windows",
@@ -251,25 +226,20 @@ def test_resolve_path_fields(self) -> None:
 
     def test_required_fields_w_class_path(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginStackDefinitionModel.parse_obj({"class_path": "something"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("name",)
-        assert errors[0]["msg"] == "field required"
+        with pytest.raises(ValidationError, match="name\n  Field required"):
+            CfnginStackDefinitionModel.model_validate({"class_path": "something"})
 
     def test_validate_class_and_template(self) -> None:
         """Test _validate_class_and_template."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError,
+            match="Value error, only one of class_path or template_path can be defined",
+        ):
             CfnginStackDefinitionModel(
                 class_path="something",
                 name="stack-name",
                 template_path="./something.yml",  # type: ignore
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("__root__",)
-        assert errors[0]["msg"] == "only one of class_path or template_path can be defined"
 
     @pytest.mark.parametrize("enabled, locked", [(True, True), (False, True), (False, False)])
     def test_validate_class_or_template(self, enabled: bool, locked: bool) -> None:
@@ -286,9 +256,7 @@ def test_validate_class_or_template(self, enabled: bool, locked: bool) -> None:
 
     def test_validate_class_or_template_invalid(self) -> None:
         """Test _validate_class_or_template invalid."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(
+            ValidationError, match="Value error, either class_path or template_path must be defined"
+        ):
             CfnginStackDefinitionModel(enabled=True, locked=False, name="stack-name")
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("__root__",)
-        assert errors[0]["msg"] == "either class_path or template_path must be defined"
diff --git a/tests/unit/config/models/cfngin/test_package_sources.py b/tests/unit/config/models/cfngin/test__package_sources.py
similarity index 65%
rename from tests/unit/config/models/cfngin/test_package_sources.py
rename to tests/unit/config/models/cfngin/test__package_sources.py
index cb22fbbe0..cd191d1b0 100644
--- a/tests/unit/config/models/cfngin/test_package_sources.py
+++ b/tests/unit/config/models/cfngin/test__package_sources.py
@@ -1,7 +1,5 @@
 """Test runway.config.models.cfngin._package_sources."""
 
-# pyright: basic
-
 import pytest
 from pydantic import ValidationError
 
@@ -18,12 +16,8 @@ class TestCfnginPackageSourcesDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnginPackageSourcesDefinitionModel.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
+            CfnginPackageSourcesDefinitionModel.model_validate({"invalid": "val"})
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
@@ -39,8 +33,8 @@ def test_fields(self) -> None:
             "local": [{"source": "something"}],
             "s3": [{"bucket": "bucket", "key": "something"}],
         }
-        obj: CfnginPackageSourcesDefinitionModel = CfnginPackageSourcesDefinitionModel.parse_obj(
-            data
+        obj: CfnginPackageSourcesDefinitionModel = (
+            CfnginPackageSourcesDefinitionModel.model_validate(data)
         )
         assert isinstance(obj.git[0], GitCfnginPackageSourceDefinitionModel)
         assert isinstance(obj.local[0], LocalCfnginPackageSourceDefinitionModel)
@@ -52,15 +46,11 @@ class TestGitCfnginPackageSourceDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             GitCfnginPackageSourceDefinitionModel(
                 invalid="something",  # type: ignore
                 uri="something",
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
@@ -74,12 +64,8 @@ def test_field_defaults(self) -> None:
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            GitCfnginPackageSourceDefinitionModel()  # type: ignore
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("uri",)
-        assert errors[0]["msg"] == "field required"
+        with pytest.raises(ValidationError, match="uri\n  Field required"):
+            GitCfnginPackageSourceDefinitionModel.model_validate({})
 
     @pytest.mark.parametrize(
         "ref",
@@ -92,7 +78,9 @@ def test_required_fields(self) -> None:
     def test_validate_one_ref(self, ref: dict[str, str]) -> None:
         """Test _validate_one_ref."""
         data = {"uri": "something", ref["field"]: ref["value"]}
-        assert GitCfnginPackageSourceDefinitionModel.parse_obj(data)[ref["field"]] == ref["value"]
+        assert (
+            GitCfnginPackageSourceDefinitionModel.model_validate(data)[ref["field"]] == ref["value"]
+        )
 
     @pytest.mark.parametrize(
         "refs",
@@ -119,12 +107,11 @@ def test_validate_one_ref(self, ref: dict[str, str]) -> None:
     def test_validate_one_ref_invalid(self, refs: list[dict[str, str]]) -> None:
         """Test _validate_one_ref invalid values."""
         data = {"uri": "something", **{ref["field"]: ref["value"] for ref in refs}}
-        with pytest.raises(ValidationError) as excinfo:
-            GitCfnginPackageSourceDefinitionModel.parse_obj(data)
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("__root__",)
-        assert errors[0]["msg"].startswith("only one of")
+        with pytest.raises(
+            ValidationError,
+            match=r"1 validation error.*\n  Value error, only one of \['branch', 'commit', 'tag'\] can be defined",
+        ):
+            GitCfnginPackageSourceDefinitionModel.model_validate(data)
 
 
 class TestLocalCfnginPackageSourceDefinitionModel:
@@ -132,15 +119,11 @@ class TestLocalCfnginPackageSourceDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             LocalCfnginPackageSourceDefinitionModel(
                 invalid="something",  # type: ignore
                 source="something",
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
@@ -151,12 +134,8 @@ def test_field_defaults(self) -> None:
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            LocalCfnginPackageSourceDefinitionModel()  # type: ignore
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("source",)
-        assert errors[0]["msg"] == "field required"
+        with pytest.raises(ValidationError, match="source\n  Field required"):
+            LocalCfnginPackageSourceDefinitionModel.model_validate({})
 
 
 class TestS3CfnginPackageSourceDefinitionModel:
@@ -164,27 +143,20 @@ class TestS3CfnginPackageSourceDefinitionModel:
 
     def test_extra(self) -> None:
         """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
             S3CfnginPackageSourceDefinitionModel(
                 bucket="something",
                 key="something",
                 invalid="something",  # type: ignore
             )
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
 
     def test_required_fields(self) -> None:
         """Test required fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            S3CfnginPackageSourceDefinitionModel()  # type: ignore
-        errors = excinfo.value.errors()
-        assert len(errors) == 2
-        assert errors[0]["loc"] == ("bucket",)
-        assert errors[0]["msg"] == "field required"
-        assert errors[1]["loc"] == ("key",)
-        assert errors[1]["msg"] == "field required"
+        with pytest.raises(
+            ValidationError,
+            match="2 validation errors.*\nbucket\n  Field required.*\n.*\nkey\n  Field required",
+        ):
+            S3CfnginPackageSourceDefinitionModel.model_validate({})
 
     def test_field_defaults(self) -> None:
         """Test field default values."""
diff --git a/tests/unit/config/models/runway/options/test_cdk.py b/tests/unit/config/models/runway/options/test__cdk.py
similarity index 85%
rename from tests/unit/config/models/runway/options/test_cdk.py
rename to tests/unit/config/models/runway/options/test__cdk.py
index 372234f50..56efb0c11 100644
--- a/tests/unit/config/models/runway/options/test_cdk.py
+++ b/tests/unit/config/models/runway/options/test__cdk.py
@@ -1,6 +1,5 @@
 """Test runway.config.models.runway.options.cdk."""
 
-# pyright: basic
 from runway.config.models.runway.options.cdk import RunwayCdkModuleOptionsDataModel
 
 
@@ -16,8 +15,8 @@ def test_init_default(self) -> None:
 
     def test_init_extra(self) -> None:
         """Test init extra."""
-        obj = RunwayCdkModuleOptionsDataModel.parse_obj({"invalid": "val"})
-        assert "invalid" not in obj.dict()
+        obj = RunwayCdkModuleOptionsDataModel.model_validate({"invalid": "val"})
+        assert "invalid" not in obj.model_dump()
 
     def test_init(self) -> None:
         """Test init."""
diff --git a/tests/unit/config/models/runway/options/test_k8s.py b/tests/unit/config/models/runway/options/test__k8s.py
similarity index 86%
rename from tests/unit/config/models/runway/options/test_k8s.py
rename to tests/unit/config/models/runway/options/test__k8s.py
index 9aa695e29..8365621c8 100644
--- a/tests/unit/config/models/runway/options/test_k8s.py
+++ b/tests/unit/config/models/runway/options/test__k8s.py
@@ -1,6 +1,5 @@
 """Test runway.config.models.runway.options.k8s."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
@@ -22,8 +21,8 @@ def test_init_default(self) -> None:
 
     def test_init_extra(self) -> None:
         """Test init extra."""
-        obj = RunwayK8sModuleOptionsDataModel.parse_obj({"invalid": "val"})
-        assert "invalid" not in obj.dict()
+        obj = RunwayK8sModuleOptionsDataModel.model_validate({"invalid": "val"})
+        assert "invalid" not in obj.model_dump()
 
     def test_init(self, tmp_path: Path) -> None:
         """Test init."""
diff --git a/tests/unit/config/models/runway/options/test_serverless.py b/tests/unit/config/models/runway/options/test__serverless.py
similarity index 92%
rename from tests/unit/config/models/runway/options/test_serverless.py
rename to tests/unit/config/models/runway/options/test__serverless.py
index 126249897..3b2939571 100644
--- a/tests/unit/config/models/runway/options/test_serverless.py
+++ b/tests/unit/config/models/runway/options/test__serverless.py
@@ -1,6 +1,5 @@
 """Test runway.config.models.runway.options.serverless."""
 
-# pyright: basic
 import pytest
 from pydantic import ValidationError
 
@@ -31,7 +30,7 @@ def test_init(self) -> None:
             "promotezip": {"bucketname": "test"},
             "skip_npm_ci": True,
         }
-        obj = RunwayServerlessModuleOptionsDataModel(**data)
+        obj = RunwayServerlessModuleOptionsDataModel.model_validate(data)
         assert obj.args == data["args"]
         assert obj.extend_serverless_yml == data["extend_serverless_yml"]
         assert obj.promotezip == RunwayServerlessPromotezipOptionDataModel(
@@ -56,7 +55,7 @@ def test_init_default(self) -> None:
     def test_init_extra(self) -> None:
         """Test init with extra values."""
         with pytest.raises(ValidationError):
-            RunwayServerlessPromotezipOptionDataModel.parse_obj({"invalid": "val"})
+            RunwayServerlessPromotezipOptionDataModel.model_validate({"invalid": "val"})
 
     def test_init(self) -> None:
         """Test init."""
diff --git a/tests/unit/config/models/runway/options/test_terraform.py b/tests/unit/config/models/runway/options/test__terraform.py
similarity index 89%
rename from tests/unit/config/models/runway/options/test_terraform.py
rename to tests/unit/config/models/runway/options/test__terraform.py
index 28e59c25d..e670fcdb6 100644
--- a/tests/unit/config/models/runway/options/test_terraform.py
+++ b/tests/unit/config/models/runway/options/test__terraform.py
@@ -1,6 +1,5 @@
 """Test runway.config.models.runway.options.terraform."""
 
-# pyright: basic
 import pytest
 from pydantic import ValidationError
 
@@ -27,7 +26,7 @@ def test_init_default(self) -> None:
     def test_init_extra(self) -> None:
         """Test init extra."""
         with pytest.raises(ValidationError):
-            RunwayTerraformArgsDataModel.parse_obj({"invalid": "val"})
+            RunwayTerraformArgsDataModel.model_validate({"invalid": "val"})
 
     def test_init(self) -> None:
         """Test init."""
@@ -61,7 +60,7 @@ def test_init_default(self) -> None:
     def test_init_extra(self) -> None:
         """Test init extra."""
         with pytest.raises(ValidationError):
-            RunwayTerraformBackendConfigDataModel.parse_obj({"invalid": "val"})
+            RunwayTerraformBackendConfigDataModel.model_validate({"invalid": "val"})
 
     def test_init(self) -> None:
         """Test init."""
@@ -71,7 +70,7 @@ def test_init(self) -> None:
             "region": "us-east-1",
             "workspace_key_prefix": "workspace_prefix",
         }
-        obj = RunwayTerraformBackendConfigDataModel.parse_obj(data)
+        obj = RunwayTerraformBackendConfigDataModel.model_validate(data)
         assert obj.bucket == data["bucket"]
         assert obj.dynamodb_table == data["dynamodb_table"]
         assert obj.region == data["region"]
@@ -83,7 +82,7 @@ class TestRunwayTerraformModuleOptionsDataModel:
 
     def test_convert_args(self) -> None:
         """Test _convert_args."""
-        obj = RunwayTerraformModuleOptionsDataModel.parse_obj({"args": ["test"]})
+        obj = RunwayTerraformModuleOptionsDataModel.model_validate({"args": ["test"]})
         assert obj.args.apply == ["test"]
         assert not obj.args.init
         assert isinstance(obj.args.init, list)
@@ -103,7 +102,7 @@ def test_init_default(self) -> None:
 
     def test_init_extra(self) -> None:
         """Test init extra."""
-        assert RunwayTerraformModuleOptionsDataModel.parse_obj({"invalid": "val"})
+        assert RunwayTerraformModuleOptionsDataModel.model_validate({"invalid": "val"})
 
     def test_init(self) -> None:
         """Test init."""
@@ -114,7 +113,7 @@ def test_init(self) -> None:
             "terraform_workspace": "default",
             "terraform_write_auto_tfvars": True,
         }
-        obj = RunwayTerraformModuleOptionsDataModel.parse_obj(data)
+        obj = RunwayTerraformModuleOptionsDataModel.model_validate(data)
         assert obj.args.init == data["args"]["init"]  # type: ignore
         assert (
             obj.backend_config.bucket == data["terraform_backend_config"]["bucket"]  # type: ignore
diff --git a/tests/unit/config/models/runway/test___init__.py b/tests/unit/config/models/runway/test___init__.py
new file mode 100644
index 000000000..3d5874016
--- /dev/null
+++ b/tests/unit/config/models/runway/test___init__.py
@@ -0,0 +1,191 @@
+"""Test runway.config.models.runway.__init__."""
+
+from pathlib import Path
+from typing import Any
+
+import pytest
+import yaml
+from packaging.specifiers import SpecifierSet
+from pydantic import ValidationError
+
+from runway.config.models.runway import (
+    RunwayAssumeRoleDefinitionModel,
+    RunwayConfigDefinitionModel,
+    RunwayDeploymentDefinitionModel,
+    RunwayFutureDefinitionModel,
+    RunwayVariablesDefinitionModel,
+)
+
+
+class TestRunwayConfigDefinitionModel:
+    """Test runway.config.models.runway.RunwayConfigDefinitionModel."""
+
+    def test_add_deployment_names(self) -> None:
+        """Test _add_deployment_names."""
+        data = {
+            "deployments": [
+                {"modules": ["sampleapp.cfn"], "regions": ["us-east-1"]},
+                {
+                    "name": "test-name",
+                    "modules": ["sampleapp.cfn"],
+                    "regions": ["us-west-2"],
+                },
+            ]
+        }
+        obj = RunwayConfigDefinitionModel.model_validate(data)
+        # this also adds coverage for __getitem__
+        assert obj["deployments"][0]["name"] == "deployment_1"
+        assert obj["deployments"][1]["name"] == "test-name"
+
+    def test_convert_runway_version(self) -> None:
+        """Test _convert_runway_version."""
+        assert RunwayConfigDefinitionModel(  # handle string
+            runway_version=">1.11.0"  # type: ignore
+        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
+        assert RunwayConfigDefinitionModel(  # handle exact version
+            runway_version="1.11.0"  # type: ignore
+        ).runway_version == SpecifierSet("==1.11.0", prereleases=True)
+        assert RunwayConfigDefinitionModel(  # handle SpecifierSet
+            runway_version=SpecifierSet(">1.11.0")  # type: ignore
+        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
+        assert RunwayConfigDefinitionModel(  # handle SpecifierSet
+            runway_version=SpecifierSet(">1.11.0", prereleases=True)  # type: ignore
+        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
+
+    def test_convert_runway_version_invalid(self) -> None:
+        """Test _convert_runway_version invalid specifier set."""
+        with pytest.raises(
+            ValidationError, match="Value error, =latest is not a valid version specifier set"
+        ):
+            RunwayConfigDefinitionModel(runway_version="=latest")  # type: ignore
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
+            RunwayConfigDefinitionModel.model_validate({"invalid": "val"})
+
+    def test_field_defaults(self) -> None:
+        """Test filed default values."""
+        obj = RunwayConfigDefinitionModel()
+        assert obj.deployments == []
+        assert isinstance(obj.future, RunwayFutureDefinitionModel)
+        assert not obj.ignore_git_branch
+        assert obj.runway_version is None
+        assert obj.tests == []
+        assert isinstance(obj.variables, RunwayVariablesDefinitionModel)
+
+    def test_parse_file(self, tmp_path: Path) -> None:
+        """Test parse_file."""
+        data = {
+            "deployments": [
+                {
+                    "name": "test-name",
+                    "modules": ["sampleapp.cfn"],
+                    "regions": ["us-east-1"],
+                },
+            ]
+        }
+        runway_yml = tmp_path / "runway.yml"
+        runway_yml.write_text(yaml.dump(data))
+
+        obj = RunwayConfigDefinitionModel.parse_file(runway_yml)
+        assert obj.deployments[0].modules[0].name == "sampleapp.cfn"
+
+
+class TestRunwayDeploymentDefinitionModel:
+    """Test runway.config.models.runway.RunwayDeploymentDefinitionModel."""
+
+    def test_convert_simple_module(self) -> None:
+        """Test _convert_simple_module."""
+        obj = RunwayDeploymentDefinitionModel(
+            modules=["sampleapp.cfn", {"path": "./"}],  # type: ignore
+            regions=["us-east-1"],
+        )
+        assert obj.modules[0].path == "sampleapp.cfn"
+        assert obj.modules[1].path == "./"
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="invalid\n  Extra inputs are not permitted"):
+            RunwayDeploymentDefinitionModel.model_validate(
+                {"invalid": "val", "regions": ["us-east-1"]}
+            )
+
+    def test_field_defaults(self) -> None:
+        """Test field default values."""
+        obj = RunwayDeploymentDefinitionModel(modules=[], regions=["us-east-1"])
+        assert obj.account_alias is None
+        assert obj.account_id is None
+        assert isinstance(obj.assume_role, RunwayAssumeRoleDefinitionModel)
+        assert obj.env_vars == {}
+        assert obj.environments == {}
+        assert obj.modules == []
+        assert obj.module_options == {}
+        assert obj.name == "unnamed_deployment"
+        assert obj.parallel_regions == []
+        assert obj.parameters == {}
+        assert obj.regions == ["us-east-1"]
+
+    @pytest.mark.parametrize(
+        "field",
+        [
+            "env_vars",
+            "environments",
+            "module_options",
+            "parallel_regions",
+            "parameters",
+            "regions",
+        ],
+    )
+    def test_fields_string_lookup_only(self, field: str) -> None:
+        """Test fields that support strings only for lookups."""
+        data: dict[str, Any] = {}
+        if field not in ["parallel_regions", "regions"]:
+            data["regions"] = ["us-east-1"]
+        data[field] = "something"
+        with pytest.raises(
+            ValidationError,
+            match=f"{field}\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayDeploymentDefinitionModel.model_validate(data)
+
+        data[field] = "${var something}"
+        obj = RunwayDeploymentDefinitionModel.model_validate(data)
+        assert obj[field] == data[field]
+
+    def test_validate_regions(self) -> None:
+        """Test _validate_regions."""
+        with pytest.raises(ValidationError):
+            RunwayDeploymentDefinitionModel(modules=[])
+        with pytest.raises(ValidationError):
+            RunwayDeploymentDefinitionModel(
+                modules=[], parallel_regions=["us-east-1"], regions=["us-east-1"]
+            )
+        with pytest.raises(ValidationError):
+            RunwayDeploymentDefinitionModel(
+                modules=[],
+                parallel_regions=["us-east-1"],
+                regions={"parallel": ["us-east-1"]},  # type: ignore
+            )
+        with pytest.raises(
+            ValidationError,
+            match="Value error, unable to validate parallel_regions/regions - both are defined as strings",
+        ):
+            RunwayDeploymentDefinitionModel(
+                modules=[], parallel_regions="something", regions="something"
+            )
+
+        obj0 = RunwayDeploymentDefinitionModel(modules=[], regions=["us-east-1"])
+        assert obj0.regions == ["us-east-1"]
+        assert obj0.parallel_regions == []
+
+        obj1 = RunwayDeploymentDefinitionModel(modules=[], parallel_regions=["us-east-1"])
+        assert obj1.regions == []
+        assert obj1.parallel_regions == ["us-east-1"]
+
+        obj2 = RunwayDeploymentDefinitionModel(
+            modules=[],
+            regions={"parallel": ["us-east-1"]},  # type: ignore
+        )
+        assert obj2.regions == []
+        assert obj2.parallel_regions == ["us-east-1"]
diff --git a/tests/unit/config/models/runway/test__assume_role.py b/tests/unit/config/models/runway/test__assume_role.py
new file mode 100644
index 000000000..6e2d6feba
--- /dev/null
+++ b/tests/unit/config/models/runway/test__assume_role.py
@@ -0,0 +1,69 @@
+"""Test runway.config.models.runway._assume_role."""
+
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.models.runway._assume_role import RunwayAssumeRoleDefinitionModel
+
+
+class TestRunwayAssumeRoleDefinitionModel:
+    """Test RunwayAssumeRoleDefinitionModel."""
+
+    @pytest.mark.parametrize("arn", ["null", "none", "None", "undefined"])
+    def test_convert_arn_null_value(self, arn: str) -> None:
+        """Test _convert_arn_null_value."""
+        assert not RunwayAssumeRoleDefinitionModel(arn=arn).arn
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
+            RunwayAssumeRoleDefinitionModel.model_validate({"invalid": "val"})
+
+    def test_field_defaults(self) -> None:
+        """Test field values."""
+        obj = RunwayAssumeRoleDefinitionModel()
+        assert not obj.arn
+        assert obj.duration == 3600
+        assert not obj.post_deploy_env_revert
+        assert obj.session_name == "runway"
+
+    def test_fields(self) -> None:
+        """Test fields."""
+        data = {
+            "arn": "test-arn",
+            "duration": 900,
+            "post_deploy_env_revert": True,
+            "session_name": "test-session",
+        }
+        obj = RunwayAssumeRoleDefinitionModel.model_validate(data)
+        assert obj.arn == data["arn"]
+        assert obj.duration == data["duration"]
+        assert obj.post_deploy_env_revert == data["post_deploy_env_revert"]
+        assert obj.session_name == data["session_name"]
+
+    def test_string_duration(self) -> None:
+        """Test duration defined as a string."""
+        with pytest.raises(
+            ValidationError,
+            match="duration\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayAssumeRoleDefinitionModel(duration="something")
+
+    def test_string_duration_lookup(self) -> None:
+        """Test duration defined as a lookup string."""
+        value = "${var something}"
+        obj = RunwayAssumeRoleDefinitionModel(duration=value)
+        assert obj.duration == value
+
+    @pytest.mark.parametrize("duration", [900, 3600, 43_200])
+    def test_validate_duration(self, duration: int) -> None:
+        """Test _validate_duration."""
+        assert RunwayAssumeRoleDefinitionModel(duration=duration).duration == duration
+
+    @pytest.mark.parametrize("duration", [899, 43_201])
+    def test_validate_duration_invalid(self, duration: int) -> None:
+        """Test _validate_duration."""
+        with pytest.raises(ValidationError, match="duration"):
+            RunwayAssumeRoleDefinitionModel(duration=duration)
diff --git a/tests/unit/config/models/runway/test__builtin_tests.py b/tests/unit/config/models/runway/test__builtin_tests.py
new file mode 100644
index 000000000..4290f0368
--- /dev/null
+++ b/tests/unit/config/models/runway/test__builtin_tests.py
@@ -0,0 +1,56 @@
+"""Test runway.config.models.runway._builtin_tests."""
+
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.models.runway import RunwayTestDefinitionModel
+
+
+class TestRunwayTestDefinitionModel:
+    """Test runway.config.models.runway._builtin_tests.RunwayTestDefinitionModel."""
+
+    def test_invalid_type(self) -> None:
+        """Test invalid type."""
+        with pytest.raises(ValidationError) as excinfo:
+            RunwayTestDefinitionModel.model_validate({"type": "invalid"})
+        assert excinfo.value.errors()[0]["loc"] == ("type",)
+
+    @pytest.mark.parametrize("required", [None, True, False])
+    def test_required(self, required: bool | None) -> None:
+        """Test required."""
+        if isinstance(required, bool):
+            obj = RunwayTestDefinitionModel(type="script", required=required)
+            assert obj.required is required
+        else:
+            obj = RunwayTestDefinitionModel(type="script")
+            assert obj.required is False
+
+    def test_string_args(self) -> None:
+        """Test args defined as a string."""
+        with pytest.raises(
+            ValidationError,
+            match="args\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayTestDefinitionModel.model_validate({"args": "something", "type": "yamllint"})
+
+    def test_string_args_lookup(self) -> None:
+        """Test args defined as a lookup string."""
+        data = {"args": "${var something}", "type": "yamllint"}
+        obj = RunwayTestDefinitionModel.model_validate(data)
+        assert obj.args == data["args"]
+
+    def test_string_required(self) -> None:
+        """Test required defined as a string."""
+        with pytest.raises(
+            ValidationError,
+            match="required\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayTestDefinitionModel.model_validate({"required": "something", "type": "yamllint"})
+
+    def test_string_required_lookup(self) -> None:
+        """Test required defined as a lookup string."""
+        data = {"required": "${var something}", "type": "yamllint"}
+        obj = RunwayTestDefinitionModel.model_validate(data)
+        assert obj.required == data["required"]
diff --git a/tests/unit/config/models/runway/test__future.py b/tests/unit/config/models/runway/test__future.py
new file mode 100644
index 000000000..646f305d2
--- /dev/null
+++ b/tests/unit/config/models/runway/test__future.py
@@ -0,0 +1,17 @@
+"""Test runway.config.models.runway._future."""
+
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.models.runway._future import RunwayFutureDefinitionModel
+
+
+class TestRunwayFutureDefinitionModel:
+    """Test RunwayFutureDefinitionModel."""
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
+            RunwayFutureDefinitionModel.model_validate({"invalid": "val"})
diff --git a/tests/unit/config/models/runway/test__module.py b/tests/unit/config/models/runway/test__module.py
new file mode 100644
index 000000000..75c45e524
--- /dev/null
+++ b/tests/unit/config/models/runway/test__module.py
@@ -0,0 +1,90 @@
+"""Test runway.config.models.runway._module."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.models.runway._module import RunwayModuleDefinitionModel
+
+
+class TestRunwayModuleDefinitionModel:
+    """Test rRunwayModuleDefinitionModel."""
+
+    def test__validate_name(self) -> None:
+        """Test _validate_name."""
+        assert RunwayModuleDefinitionModel().name == "runway"
+        assert RunwayModuleDefinitionModel(name="test-name").name == "test-name"
+        assert (
+            RunwayModuleDefinitionModel(parallel=[{"path": "./"}]).name  # type: ignore
+            == "parallel_parent"
+        )
+        assert (
+            RunwayModuleDefinitionModel(
+                name="something",
+                parallel=[{"path": "./"}],  # type: ignore
+            ).name
+            == "something"
+        )
+        assert RunwayModuleDefinitionModel(path="./").name == Path.cwd().resolve().name
+
+    def test__validate_parallel(self) -> None:
+        """Test _validate_parallel."""
+        with pytest.raises(
+            ValidationError,
+            match="parallel\n  Value error, only one of parallel or path can be defined",
+        ):
+            RunwayModuleDefinitionModel(
+                path=Path.cwd(),
+                parallel=["./"],  # type: ignore
+            )
+
+        assert RunwayModuleDefinitionModel().parallel == []
+        assert RunwayModuleDefinitionModel(parallel=["./"]).parallel == [  # type: ignore
+            RunwayModuleDefinitionModel(path="./")
+        ]
+        assert RunwayModuleDefinitionModel(
+            parallel=[{"name": "test", "path": "./"}]  # type: ignore
+        ).parallel == [RunwayModuleDefinitionModel(name="test", path="./")]
+
+    def test__validate_path(self) -> None:
+        """Test _validate_path."""
+        assert RunwayModuleDefinitionModel().path == Path.cwd()
+        assert not RunwayModuleDefinitionModel(parallel=[{"path": "./"}]).path  # type: ignore
+        defined_path = Path("./sampleapp.cfn")
+        assert RunwayModuleDefinitionModel(path=defined_path).path == defined_path
+
+    @pytest.mark.parametrize("field", ["env_vars", "environments", "options", "parameters"])
+    def test__validate_string_is_lookup(self, field: str) -> None:
+        """Test fields that support strings only for lookups."""
+        data = {field: "something"}
+        with pytest.raises(
+            ValidationError,
+            match=f"{field}\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayModuleDefinitionModel.model_validate(data)
+
+        data[field] = "${var something}"
+        obj = RunwayModuleDefinitionModel.model_validate(data)
+        assert obj[field] == data[field]
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
+            RunwayModuleDefinitionModel.model_validate({"invalid": "val"})
+
+    def test_field_defaults(self) -> None:
+        """Test field defaults."""
+        obj = RunwayModuleDefinitionModel()
+        assert not obj.class_path
+        assert obj.environments == {}
+        assert obj.env_vars == {}
+        assert obj.name == "runway"
+        assert obj.options == {}
+        assert obj.parameters == {}
+        assert obj.path == Path.cwd()
+        assert obj.tags == []
+        assert obj.type is None
+        assert obj.parallel == []
diff --git a/tests/unit/config/models/runway/test__region.py b/tests/unit/config/models/runway/test__region.py
new file mode 100644
index 000000000..3cec3ab34
--- /dev/null
+++ b/tests/unit/config/models/runway/test__region.py
@@ -0,0 +1,37 @@
+"""Test runway.config.models.runway._deployment_region."""
+
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from runway.config.models.runway._region import RunwayDeploymentRegionDefinitionModel
+
+
+class TestRunwayDeploymentRegionDefinitionModel:
+    """Test RunwayDeploymentRegionDefinitionModel."""
+
+    def test_extra(self) -> None:
+        """Test extra fields."""
+        with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
+            RunwayDeploymentRegionDefinitionModel.model_validate({"invalid": "val", "parallel": []})
+
+    def test_fields(self) -> None:
+        """Test fields."""
+        assert not RunwayDeploymentRegionDefinitionModel(parallel=[]).parallel
+        value = ["us-east-1", "us-west-2"]
+        assert RunwayDeploymentRegionDefinitionModel(parallel=value).parallel == value
+
+    def test_string_parallel(self) -> None:
+        """Test parallel defined as a string."""
+        with pytest.raises(
+            ValidationError,
+            match="parallel\n  Value error, field can only be a string if it's a lookup",
+        ):
+            RunwayDeploymentRegionDefinitionModel(parallel="something")
+
+    def test_string_parallel_lookup(self) -> None:
+        """Test parallel defined as a lookup string."""
+        value = "${var something}"
+        obj = RunwayDeploymentRegionDefinitionModel(parallel=value)
+        assert obj.parallel == value
diff --git a/tests/unit/config/models/runway/test__variables.py b/tests/unit/config/models/runway/test__variables.py
new file mode 100644
index 000000000..9bcabd731
--- /dev/null
+++ b/tests/unit/config/models/runway/test__variables.py
@@ -0,0 +1,1 @@
+"""Test runway.config.models.runway._variables."""
diff --git a/tests/unit/config/models/runway/test_builtin_tests.py b/tests/unit/config/models/runway/test_builtin_tests.py
deleted file mode 100644
index b4f2f01fc..000000000
--- a/tests/unit/config/models/runway/test_builtin_tests.py
+++ /dev/null
@@ -1,168 +0,0 @@
-"""Test runway.config.models.runway._builtin_tests."""
-
-# pyright: basic
-from typing import Optional
-
-import pytest
-from pydantic import ValidationError
-
-from runway.config.models.runway import (
-    CfnLintRunwayTestArgs,
-    CfnLintRunwayTestDefinitionModel,
-    RunwayTestDefinitionModel,
-    ScriptRunwayTestArgs,
-    ScriptRunwayTestDefinitionModel,
-    YamlLintRunwayTestDefinitionModel,
-)
-
-
-class TestRunwayTestDefinitionModel:
-    """Test runway.config.models.runway._builtin_tests.RunwayTestDefinitionModel."""
-
-    def test_init_cfnlint(self) -> None:
-        """Test init cfn-lint subclass."""
-        data = {"type": "cfn-lint"}
-        obj = RunwayTestDefinitionModel.parse_obj(data)
-
-        assert isinstance(obj, CfnLintRunwayTestDefinitionModel)
-        assert obj.args.dict() == {"cli_args": []}
-        assert obj.name == "cfn-lint"
-        assert obj.type == "cfn-lint"
-
-    def test_init_script(self) -> None:
-        """Test init script subclass."""
-        data = {"type": "script"}
-        obj = RunwayTestDefinitionModel.parse_obj(data)
-
-        assert isinstance(obj, ScriptRunwayTestDefinitionModel)
-        assert obj.args.dict() == {"commands": []}
-        assert obj.name == "script"
-        assert obj.type == "script"
-
-    def test_init_yamllint(self) -> None:
-        """Test init yamllint subclass."""
-        data = {"type": "yamllint"}
-        obj = RunwayTestDefinitionModel.parse_obj(data)
-
-        assert isinstance(obj, YamlLintRunwayTestDefinitionModel)
-        assert obj.args == {}
-        assert obj.name == "yamllint"
-        assert obj.type == "yamllint"
-
-    def test_invalid_type(self) -> None:
-        """Test invalid type."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayTestDefinitionModel.parse_obj({"type": "invalid"})
-        assert excinfo.value.errors()[0]["loc"] == ("type",)
-
-    @pytest.mark.parametrize("required", [None, True, False])
-    def test_required(self, required: Optional[bool]) -> None:
-        """Test required."""
-        if isinstance(required, bool):
-            obj = RunwayTestDefinitionModel(type="script", required=required)
-            assert obj.required is required
-        else:
-            obj = RunwayTestDefinitionModel(type="script")
-            assert obj.required is False
-
-    def test_string_args(self) -> None:
-        """Test args defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayTestDefinitionModel.parse_obj({"args": "something", "type": "yamllint"})
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("args",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_string_args_lookup(self) -> None:
-        """Test args defined as a lookup string."""
-        data = {"args": "${var something}", "type": "yamllint"}
-        obj = RunwayTestDefinitionModel.parse_obj(data)
-        assert obj.args == data["args"]
-
-    def test_string_required(self) -> None:
-        """Test required defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayTestDefinitionModel.parse_obj({"required": "something", "type": "yamllint"})
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("required",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_string_required_lookup(self) -> None:
-        """Test required defined as a lookup string."""
-        data = {"required": "${var something}", "type": "yamllint"}
-        obj = RunwayTestDefinitionModel.parse_obj(data)
-        assert obj.required == data["required"]
-
-
-class TestCfnLintRunwayTestArgs:
-    """Test runway.config.models.runway._builtin_tests.CfnLintRunwayTestArgs."""
-
-    def test_cli_args_string(self) -> None:
-        """Test cli_args defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnLintRunwayTestArgs(cli_args="something")
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("cli_args",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_cli_args_string_lookup(self) -> None:
-        """Test args defined as a lookup string."""
-        data = {"cli_args": "${var something}"}
-        assert CfnLintRunwayTestArgs.parse_obj(data).cli_args == data["cli_args"]
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            CfnLintRunwayTestArgs.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-
-class TestCfnLintRunwayTestDefinitionModel:
-    """Test runway.config.models.runway._builtin_tests.CfnLintRunwayTestDefinitionModel."""
-
-    def test_args(self) -> None:
-        """Test args."""
-        data = {"args": {"cli_args": ["example"]}, "type": "cfn-lint"}
-        obj = CfnLintRunwayTestDefinitionModel.parse_obj(data)
-        assert isinstance(obj.args, CfnLintRunwayTestArgs)
-        assert obj.args.cli_args == data["args"]["cli_args"]  # type: ignore
-
-
-class TestScriptRunwayTestArgs:
-    """Test runway.config.models.runway._builtin_tests.ScriptRunwayTestArgs."""
-
-    def test_commands_string(self) -> None:
-        """Test commands defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            ScriptRunwayTestArgs(commands="something")
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("commands",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_commands_string_lookup(self) -> None:
-        """Test args defined as a lookup string."""
-        data = {"commands": "${var something}"}
-        assert ScriptRunwayTestArgs.parse_obj(data).commands == data["commands"]
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            ScriptRunwayTestArgs.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-
-class TestScriptRunwayTestDefinitionModel:
-    """Test runway.config.models.runway._builtin_tests.ScriptRunwayTestDefinitionModel."""
-
-    def test_args(self) -> None:
-        """Test args."""
-        data = {"args": {"commands": ["example"]}}
-        obj = ScriptRunwayTestDefinitionModel.parse_obj(data)
-        assert isinstance(obj.args, ScriptRunwayTestArgs)
-        assert obj.args.commands == data["args"]["commands"]
diff --git a/tests/unit/config/models/runway/test_runway.py b/tests/unit/config/models/runway/test_runway.py
deleted file mode 100644
index 4697f5966..000000000
--- a/tests/unit/config/models/runway/test_runway.py
+++ /dev/null
@@ -1,396 +0,0 @@
-"""Test runway.config.models.runway.__init__."""
-
-# pyright: basic
-from pathlib import Path
-from typing import Any
-
-import pytest
-import yaml
-from packaging.specifiers import SpecifierSet
-from pydantic import ValidationError
-
-from runway.config.models.runway import (
-    RunwayAssumeRoleDefinitionModel,
-    RunwayConfigDefinitionModel,
-    RunwayDeploymentDefinitionModel,
-    RunwayDeploymentRegionDefinitionModel,
-    RunwayFutureDefinitionModel,
-    RunwayModuleDefinitionModel,
-    RunwayVariablesDefinitionModel,
-)
-
-
-class TestRunwayAssumeRoleDefinitionModel:
-    """Test runway.config.models.runway.RunwayAssumeRoleDefinitionModel."""
-
-    @pytest.mark.parametrize("arn", ["null", "none", "None", "undefined"])
-    def test_convert_arn_null_value(self, arn: str) -> None:
-        """Test _convert_arn_null_value."""
-        assert not RunwayAssumeRoleDefinitionModel(arn=arn).arn
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayAssumeRoleDefinitionModel.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-    def test_field_defaults(self) -> None:
-        """Test field values."""
-        obj = RunwayAssumeRoleDefinitionModel()
-        assert not obj.arn
-        assert obj.duration == 3600
-        assert not obj.post_deploy_env_revert
-        assert obj.session_name == "runway"
-
-    def test_fields(self) -> None:
-        """Test fields."""
-        data = {
-            "arn": "test-arn",
-            "duration": 900,
-            "post_deploy_env_revert": True,
-            "session_name": "test-session",
-        }
-        obj = RunwayAssumeRoleDefinitionModel.parse_obj(data)
-        assert obj.arn == data["arn"]
-        assert obj.duration == data["duration"]
-        assert obj.post_deploy_env_revert == data["post_deploy_env_revert"]
-        assert obj.session_name == data["session_name"]
-
-    def test_string_duration(self) -> None:
-        """Test duration defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayAssumeRoleDefinitionModel(duration="something")
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("duration",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_string_duration_lookup(self) -> None:
-        """Test duration defined as a lookup string."""
-        value = "${var something}"
-        obj = RunwayAssumeRoleDefinitionModel(duration=value)
-        assert obj.duration == value
-
-    @pytest.mark.parametrize("duration", [900, 3600, 43_200])
-    def test_validate_duration(self, duration: int) -> None:
-        """Test _validate_duration."""
-        assert RunwayAssumeRoleDefinitionModel(duration=duration).duration == duration
-
-    @pytest.mark.parametrize("duration", [899, 43_201])
-    def test_validate_duration_invalid(self, duration: int) -> None:
-        """Test _validate_duration."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayAssumeRoleDefinitionModel(duration=duration)
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("duration",)
-
-
-class TestRunwayConfigDefinitionModel:
-    """Test runway.config.models.runway.RunwayConfigDefinitionModel."""
-
-    def test_add_deployment_names(self) -> None:
-        """Test _add_deployment_names."""
-        data = {
-            "deployments": [
-                {"modules": ["sampleapp.cfn"], "regions": ["us-east-1"]},
-                {
-                    "name": "test-name",
-                    "modules": ["sampleapp.cfn"],
-                    "regions": ["us-west-2"],
-                },
-            ]
-        }
-        obj = RunwayConfigDefinitionModel.parse_obj(data)
-        # this also adds coverage for __getitem__
-        assert obj["deployments"][0]["name"] == "deployment_1"
-        assert obj["deployments"][1]["name"] == "test-name"
-
-    def test_convert_runway_version(self) -> None:
-        """Test _convert_runway_version."""
-        assert RunwayConfigDefinitionModel(  # handle string
-            runway_version=">1.11.0"  # type: ignore
-        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
-        assert RunwayConfigDefinitionModel(  # handle exact version
-            runway_version="1.11.0"  # type: ignore
-        ).runway_version == SpecifierSet("==1.11.0", prereleases=True)
-        assert RunwayConfigDefinitionModel(  # handle SpecifierSet
-            runway_version=SpecifierSet(">1.11.0")  # type: ignore
-        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
-        assert RunwayConfigDefinitionModel(  # handle SpecifierSet
-            runway_version=SpecifierSet(">1.11.0", prereleases=True)  # type: ignore
-        ).runway_version == SpecifierSet(">1.11.0", prereleases=True)
-
-    def test_convert_runway_version_invalid(self) -> None:
-        """Test _convert_runway_version invalid specifier set."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayConfigDefinitionModel(runway_version="=latest")  # type: ignore
-        assert excinfo.value.errors()[0]["msg"] == "=latest is not a valid version specifier set"
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayConfigDefinitionModel.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-    def test_field_defaults(self) -> None:
-        """Test filed default values."""
-        obj = RunwayConfigDefinitionModel()
-        assert obj.deployments == []
-        assert isinstance(obj.future, RunwayFutureDefinitionModel)
-        assert not obj.ignore_git_branch
-        assert obj.runway_version is None
-        assert obj.tests == []
-        assert isinstance(obj.variables, RunwayVariablesDefinitionModel)
-
-    def test_parse_file(self, tmp_path: Path) -> None:
-        """Test parse_file."""
-        data = {
-            "deployments": [
-                {
-                    "name": "test-name",
-                    "modules": ["sampleapp.cfn"],
-                    "regions": ["us-east-1"],
-                },
-            ]
-        }
-        runway_yml = tmp_path / "runway.yml"
-        runway_yml.write_text(yaml.dump(data))
-
-        obj = RunwayConfigDefinitionModel.parse_file(runway_yml)
-        assert obj.deployments[0].modules[0].name == "sampleapp.cfn"
-
-
-class TestRunwayDeploymentDefinitionModel:
-    """Test runway.config.models.runway.RunwayDeploymentDefinitionModel."""
-
-    def test_convert_simple_module(self) -> None:
-        """Test _convert_simple_module."""
-        obj = RunwayDeploymentDefinitionModel(
-            modules=["sampleapp.cfn", {"path": "./"}],  # type: ignore
-            regions=["us-east-1"],
-        )
-        assert obj.modules[0].path == "sampleapp.cfn"
-        assert obj.modules[1].path == "./"
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayDeploymentDefinitionModel.parse_obj({"invalid": "val", "regions": ["us-east-1"]})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-    def test_field_defaults(self) -> None:
-        """Test field default values."""
-        obj = RunwayDeploymentDefinitionModel(modules=[], regions=["us-east-1"])
-        assert obj.account_alias is None
-        assert obj.account_id is None
-        assert isinstance(obj.assume_role, RunwayAssumeRoleDefinitionModel)
-        assert obj.env_vars == {}
-        assert obj.environments == {}
-        assert obj.modules == []
-        assert obj.module_options == {}
-        assert obj.name == "unnamed_deployment"
-        assert obj.parallel_regions == []
-        assert obj.parameters == {}
-        assert obj.regions == ["us-east-1"]
-
-    @pytest.mark.parametrize(
-        "field",
-        [
-            "env_vars",
-            "environments",
-            "module_options",
-            "parallel_regions",
-            "parameters",
-            "regions",
-        ],
-    )
-    def test_fields_string_lookup_only(self, field: str) -> None:
-        """Test fields that support strings only for lookups."""
-        data: dict[str, Any] = {}
-        if field not in ["parallel_regions", "regions"]:
-            data["regions"] = ["us-east-1"]
-        data[field] = "something"
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayDeploymentDefinitionModel.parse_obj(data)
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == (field,)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-        data[field] = "${var something}"
-        obj = RunwayDeploymentDefinitionModel.parse_obj(data)
-        assert obj[field] == data[field]
-
-    def test_validate_regions(self) -> None:
-        """Test _validate_regions."""
-        with pytest.raises(ValidationError):
-            RunwayDeploymentDefinitionModel(modules=[])
-        with pytest.raises(ValidationError):
-            RunwayDeploymentDefinitionModel(
-                modules=[], parallel_regions=["us-east-1"], regions=["us-east-1"]
-            )
-        with pytest.raises(ValidationError):
-            RunwayDeploymentDefinitionModel(
-                modules=[],
-                parallel_regions=["us-east-1"],
-                regions={"parallel": ["us-east-1"]},  # type: ignore
-            )
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayDeploymentDefinitionModel(
-                modules=[], parallel_regions="something", regions="something"
-            )
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("__root__",)
-        assert error["msg"].startswith("unable to validate parallel_regions/regions")
-
-        obj0 = RunwayDeploymentDefinitionModel(modules=[], regions=["us-east-1"])
-        assert obj0.regions == ["us-east-1"]
-        assert obj0.parallel_regions == []
-
-        obj1 = RunwayDeploymentDefinitionModel(modules=[], parallel_regions=["us-east-1"])
-        assert obj1.regions == []
-        assert obj1.parallel_regions == ["us-east-1"]
-
-        obj2 = RunwayDeploymentDefinitionModel(
-            modules=[],
-            regions={"parallel": ["us-east-1"]},  # type: ignore
-        )
-        assert obj2.regions == []
-        assert obj2.parallel_regions == ["us-east-1"]
-
-
-class TestRunwayDeploymentRegionDefinitionModel:
-    """Test runway.config.models.runway.RunwayDeploymentRegionDefinitionModel."""
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayDeploymentRegionDefinitionModel.parse_obj({"invalid": "val", "parallel": []})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-    def test_fields(self) -> None:
-        """Test fields."""
-        assert not RunwayDeploymentRegionDefinitionModel(parallel=[]).parallel
-        value = ["us-east-1", "us-west-2"]
-        assert RunwayDeploymentRegionDefinitionModel(parallel=value).parallel == value
-
-    def test_string_parallel(self) -> None:
-        """Test parallel defined as a string."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayDeploymentRegionDefinitionModel(parallel="something")
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("parallel",)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-    def test_string_parallel_lookup(self) -> None:
-        """Test parallel defined as a lookup string."""
-        value = "${var something}"
-        obj = RunwayDeploymentRegionDefinitionModel(parallel=value)
-        assert obj.parallel == value
-
-
-class TestRunwayFutureDefinitionModel:
-    """Test runway.config.models.runway.RunwayFutureDefinitionModel."""
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayFutureDefinitionModel.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-
-class TestRunwayModuleDefinitionModel:
-    """Test runway.config.models.runway.RunwayModuleDefinitionModel."""
-
-    def test_extra(self) -> None:
-        """Test extra fields."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayModuleDefinitionModel.parse_obj({"invalid": "val"})
-        errors = excinfo.value.errors()
-        assert len(errors) == 1
-        assert errors[0]["loc"] == ("invalid",)
-        assert errors[0]["msg"] == "extra fields not permitted"
-
-    def test_field_defaults(self) -> None:
-        """Test field defaults."""
-        obj = RunwayModuleDefinitionModel()
-        assert not obj.class_path
-        assert obj.environments == {}
-        assert obj.env_vars == {}
-        assert obj.name == "undefined"
-        assert obj.options == {}
-        assert obj.parameters == {}
-        assert obj.path == Path.cwd()
-        assert obj.tags == []
-        assert obj.type is None
-        assert obj.parallel == []
-
-    def test_validate_name(self) -> None:
-        """Test _validate_name."""
-        assert RunwayModuleDefinitionModel().name == "undefined"
-        assert RunwayModuleDefinitionModel(name="test-name").name == "test-name"
-        assert (
-            RunwayModuleDefinitionModel(parallel=[{"path": "./"}]).name  # type: ignore
-            == "parallel_parent"
-        )
-        assert (
-            RunwayModuleDefinitionModel(
-                name="something",
-                parallel=[{"path": "./"}],  # type: ignore
-            ).name
-            == "something"
-        )
-        assert RunwayModuleDefinitionModel(path="./").name == Path.cwd().resolve().name
-
-    def test_validate_path(self) -> None:
-        """Test _validate_path."""
-        assert RunwayModuleDefinitionModel().path == Path.cwd()
-        assert not RunwayModuleDefinitionModel(parallel=[{"path": "./"}]).path  # type: ignore
-        defined_path = Path("./sampleapp.cfn")
-        assert RunwayModuleDefinitionModel(path=defined_path).path == defined_path
-
-    def test_validate_parallel(self) -> None:
-        """Test _validate_parallel."""
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayModuleDefinitionModel(
-                path=Path.cwd(),
-                parallel=["./"],  # type: ignore
-            )
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == ("parallel",)
-        assert error["msg"] == "only one of parallel or path can be defined"
-
-        assert RunwayModuleDefinitionModel().parallel == []
-        assert RunwayModuleDefinitionModel(parallel=["./"]).parallel == [  # type: ignore
-            RunwayModuleDefinitionModel(path="./")
-        ]
-        assert RunwayModuleDefinitionModel(
-            parallel=[{"name": "test", "path": "./"}]  # type: ignore
-        ).parallel == [RunwayModuleDefinitionModel(name="test", path="./")]
-
-    @pytest.mark.parametrize("field", ["env_vars", "environments", "options", "parameters"])
-    def test_fields_string_lookup_only(self, field: str) -> None:
-        """Test fields that support strings only for lookups."""
-        data = {field: "something"}
-        with pytest.raises(ValidationError) as excinfo:
-            RunwayModuleDefinitionModel.parse_obj(data)
-        error = excinfo.value.errors()[0]
-        assert error["loc"] == (field,)
-        assert error["msg"] == "field can only be a string if it's a lookup"
-
-        data[field] = "${var something}"
-        obj = RunwayModuleDefinitionModel.parse_obj(data)
-        assert obj[field] == data[field]
diff --git a/tests/unit/config/models/test_base.py b/tests/unit/config/models/test_base.py
index 78ff62582..0cf1e5960 100644
--- a/tests/unit/config/models/test_base.py
+++ b/tests/unit/config/models/test_base.py
@@ -1,10 +1,11 @@
 """Test runway.config.models.base."""
 
-# pyright: basic
-from typing import Any, Optional
+from __future__ import annotations
+
+from typing import Any
 
 import pytest
-from pydantic import Extra, ValidationError
+from pydantic import ValidationError
 
 from runway.config.models.base import ConfigProperty
 
@@ -18,11 +19,6 @@ class BadObject(ConfigProperty):
 
     name: str = ("invalid",)  # type: ignore
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
-
-        extra = Extra.forbid
-
 
 class GoodObject(ConfigProperty):
     """Subclass used to test a parent class.
@@ -34,12 +30,10 @@ class GoodObject(ConfigProperty):
     name: str
     bool_field: bool = True
     dict_field: dict[str, Any] = {}
-    optional_str_field: Optional[str] = None
+    optional_str_field: str | None = None
 
-    class Config(ConfigProperty.Config):
-        """Model configuration."""
 
-        extra = Extra.forbid
+GoodObject.model_config["extra"] = "forbid"
 
 
 class TestConfigProperty:
@@ -76,7 +70,7 @@ def test_validate_all(self) -> None:
         errors = excinfo.value.errors()
         assert len(errors) == 1
         assert errors[0]["loc"] == ("name",)
-        assert errors[0]["msg"] == "str type expected"
+        assert errors[0]["msg"] == "Input should be a valid string"
 
     def test_validate_assignment(self) -> None:
         """Test Config.validate_assignment."""
@@ -85,4 +79,4 @@ def test_validate_assignment(self) -> None:
         errors = excinfo.value.errors()
         assert len(errors) == 1
         assert errors[0]["loc"] == ("name",)
-        assert errors[0]["msg"] == "str type expected"
+        assert errors[0]["msg"] == "Input should be a valid string"
diff --git a/tests/unit/config/models/test_utils.py b/tests/unit/config/models/test_utils.py
index 33d5b0b0f..b6f0bd353 100644
--- a/tests/unit/config/models/test_utils.py
+++ b/tests/unit/config/models/test_utils.py
@@ -1,8 +1,9 @@
 """Test runway.config.models.utils."""
 
-# pyright: basic
+from __future__ import annotations
+
 from pathlib import Path
-from typing import Any, Optional
+from typing import Any
 
 import pytest
 
@@ -36,7 +37,7 @@ def test_convert_null_values(provided: Any, expected: Any) -> None:
 
 
 @pytest.mark.parametrize("provided", [None, Path("./")])
-def test_resolve_path_field(provided: Optional[Path]) -> None:
+def test_resolve_path_field(provided: Path | None) -> None:
     """Test resolve_path_field."""
     if provided is None:
         assert not resolve_path_field(provided)
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test___init__.py
similarity index 97%
rename from tests/unit/config/test_config.py
rename to tests/unit/config/test___init__.py
index 5f5d81bc3..ed47e10e5 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test___init__.py
@@ -1,6 +1,5 @@
-"""Test runway.config."""
+"""Test runway.config.__init__."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
@@ -37,7 +36,7 @@ class TestBaseConfig:
     def test_dump(self, monkeypatch: pytest.MonkeyPatch) -> None:
         """Test dump."""
         mock_dict = MagicMock(return_value={"name": "test"})
-        monkeypatch.setattr(ExampleModel, "dict", mock_dict)
+        monkeypatch.setattr(ExampleModel, "model_dump", mock_dict)
         obj = BaseConfig(ExampleModel())
         assert obj.dump() == "name: test\n"
         mock_dict.assert_called_once_with(
@@ -154,7 +153,7 @@ def test_parse_file_value_error(self) -> None:
     def test_parse_obj(self, monkeypatch: pytest.MonkeyPatch) -> None:
         """Test model_validate."""
         monkeypatch.setattr(
-            MODULE + ".CfnginConfigDefinitionModel.parse_obj",
+            MODULE + ".CfnginConfigDefinitionModel.model_validate",
             lambda x: CfnginConfigDefinitionModel(namespace="success"),  # type: ignore  # noqa: ARG005
         )
         assert CfnginConfig.parse_obj({}).namespace == "success"
@@ -230,7 +229,9 @@ def test_process_package_sources(
             == "rendered"
         )
         mock_source_processor.assert_called_with(
-            sources=CfnginPackageSourcesDefinitionModel.parse_obj({"git": [{"uri": "something"}]}),
+            sources=CfnginPackageSourcesDefinitionModel.model_validate(
+                {"git": [{"uri": "something"}]}
+            ),
             cache_dir=tmp_path / "cache",
         )
         assert mock_source_processor.call_count == 2
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 6c0e32b9f..e9eb954c8 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -5,7 +5,7 @@
 import logging
 import os
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import MagicMock, Mock
 
 import pytest
@@ -55,7 +55,7 @@ def aws_credentials() -> Iterator[None]:
         "AWS_SECRET_ACCESS_KEY": "testing",
         "AWS_DEFAULT_REGION": "us-east-1",
     }
-    saved_env: dict[str, Optional[str]] = {}
+    saved_env: dict[str, str | None] = {}
     for key, value in overrides.items():
         LOGGER.info("Overriding env var: %s=%s", key, value)
         saved_env[key] = os.environ.get(key, None)
diff --git a/tests/unit/context/test_base.py b/tests/unit/context/test_base.py
index 1123e937b..a0912102c 100644
--- a/tests/unit/context/test_base.py
+++ b/tests/unit/context/test_base.py
@@ -1,6 +1,5 @@
 """Test runway.context._base."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, cast
diff --git a/tests/unit/context/test_cfngin.py b/tests/unit/context/test_cfngin.py
index 11c16884f..d23a23cfc 100644
--- a/tests/unit/context/test_cfngin.py
+++ b/tests/unit/context/test_cfngin.py
@@ -1,12 +1,11 @@
 """Test runway.context._cfngin."""
 
-# pyright: basic
 from __future__ import annotations
 
 import io
 import json
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import MagicMock
 
 import pytest
@@ -46,7 +45,7 @@ def gen_tagset(tags: dict[str, str]) -> TagSetTypeDef:
     return [{"Key": key, "Value": value} for key, value in tags.items()]
 
 
-def gen_s3_object_content(content: Union[dict[str, Any], str]) -> StreamingBody:
+def gen_s3_object_content(content: dict[str, Any] | str) -> StreamingBody:
     """Convert a string or dict to S3 object body.
 
     Args:
@@ -68,7 +67,7 @@ def gen_s3_object_content(content: Union[dict[str, Any], str]) -> StreamingBody:
         ("-", None, "test"),
     ],
 )
-def test_get_fqn(delim: str, expected: str, name: Optional[str]) -> None:
+def test_get_fqn(delim: str, expected: str, name: str | None) -> None:
     """Test runway.context._cfngin.get_fqn."""
     assert get_fqn("test", delim, name) == expected
 
@@ -568,7 +567,7 @@ def test_set_hook_data_key_error(self) -> None:
     def test_set_hook_data_type_error(self, data: Any) -> None:
         """Test set_hook_data TypeError."""
         with pytest.raises(TypeError):
-            CfnginContext().set_hook_data("test", data)  # type: ignore
+            CfnginContext().set_hook_data("test", data)
 
     def test_set_hook_data(self) -> None:
         """Test set_hook_data."""
diff --git a/tests/unit/context/test_runway.py b/tests/unit/context/test_runway.py
index 4a37815ba..76e2ba133 100644
--- a/tests/unit/context/test_runway.py
+++ b/tests/unit/context/test_runway.py
@@ -1,6 +1,5 @@
 """Test runway.context._runway."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
diff --git a/tests/unit/core/components/test_deploy_environment.py b/tests/unit/core/components/test__deploy_environment.py
similarity index 100%
rename from tests/unit/core/components/test_deploy_environment.py
rename to tests/unit/core/components/test__deploy_environment.py
diff --git a/tests/unit/core/components/test_deployment.py b/tests/unit/core/components/test__deployment.py
similarity index 100%
rename from tests/unit/core/components/test_deployment.py
rename to tests/unit/core/components/test__deployment.py
diff --git a/tests/unit/core/components/test_module.py b/tests/unit/core/components/test__module.py
similarity index 99%
rename from tests/unit/core/components/test_module.py
rename to tests/unit/core/components/test__module.py
index 4e71370ac..b8194714a 100644
--- a/tests/unit/core/components/test_module.py
+++ b/tests/unit/core/components/test__module.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Optional, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import MagicMock, call
 
 import pytest
@@ -233,7 +233,7 @@ def test_should_skip(
         fx_deployments: YamlLoaderDeployment,
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
-        validate: Optional[bool],
+        validate: bool | None,
     ) -> None:
         """Test should_skip."""
         mocker.patch.object(Module, "environment_matches_defined", validate)
@@ -381,7 +381,7 @@ def test_deploy_sync(
         )
         assert not mod.deploy()
         assert "parallel_parent:processing modules sequentially..." in caplog.messages
-        mock_run.assert_has_calls([call("deploy"), call("deploy")])  # type: ignore
+        mock_run.assert_has_calls([call("deploy"), call("deploy")])
 
     @pytest.mark.parametrize("async_used", [(True), (False)])
     def test_destroy(
@@ -634,7 +634,7 @@ def test_validate_environment(
     caplog: pytest.LogCaptureFixture,
     env_def: Any,
     expected_logs: list[str],
-    expected: Optional[bool],
+    expected: bool | None,
     mocker: MockerFixture,
     runway_context: MockRunwayContext,
 ) -> None:
@@ -642,7 +642,7 @@ def test_validate_environment(
     caplog.set_level(logging.DEBUG, logger="runway")
     mocker.patch(
         f"{MODULE}.aws",
-        **{"AccountDetails.return_value": MagicMock(id="123456789012")},
+        **{"AccountDetails.return_value": MagicMock(id="123456789012")},  # type: ignore
     )
     assert validate_environment(runway_context, env_def) is expected
     # all() does not give an output that can be used for troubleshooting failures
diff --git a/tests/unit/core/components/test_module_path.py b/tests/unit/core/components/test__module_path.py
similarity index 98%
rename from tests/unit/core/components/test_module_path.py
rename to tests/unit/core/components/test__module_path.py
index c044fd5b0..c88b71ab2 100644
--- a/tests/unit/core/components/test_module_path.py
+++ b/tests/unit/core/components/test__module_path.py
@@ -1,11 +1,10 @@
 """Test runway.core.components._module_path."""
 
-# pyright: basic
 from __future__ import annotations
 
 from copy import deepcopy
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
 from unittest.mock import MagicMock
 
 import pytest
@@ -31,7 +30,7 @@ class TypeDefTestDefinitionExpected(TypedDict):  # noqa: D101
 
 
 class TypeDefTestDefinition(TypedDict):  # noqa: D101
-    definition: Optional[Union[Path, str]]
+    definition: Path | str | None
     expected: TypeDefTestDefinitionExpected
 
 
diff --git a/tests/unit/core/components/test_module_type.py b/tests/unit/core/components/test__module_type.py
similarity index 99%
rename from tests/unit/core/components/test_module_type.py
rename to tests/unit/core/components/test__module_type.py
index 11cdd27e4..10b4c84bb 100644
--- a/tests/unit/core/components/test_module_type.py
+++ b/tests/unit/core/components/test__module_type.py
@@ -1,6 +1,5 @@
 """Test runway.core.components._module_type."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/core/providers/aws/s3/_helpers/factories.py b/tests/unit/core/providers/aws/s3/_helpers/factories.py
index 401aa748a..39563309b 100644
--- a/tests/unit/core/providers/aws/s3/_helpers/factories.py
+++ b/tests/unit/core/providers/aws/s3/_helpers/factories.py
@@ -2,13 +2,13 @@
 
 from __future__ import annotations
 
-from typing import Any, Optional
+from typing import Any
 
 
 class FakeTransferFutureCallArgs:
     """Fake TransferFutureCallArgs."""
 
-    def __init__(self, *, extra_args: Optional[dict[str, Any]] = None, **kwargs: Any) -> None:
+    def __init__(self, *, extra_args: dict[str, Any] | None = None, **kwargs: Any) -> None:
         """Instantiate class."""
         self.extra_args = extra_args or {}
         for kwarg, val in kwargs.items():
@@ -20,9 +20,9 @@ class FakeTransferFutureMeta:
 
     def __init__(
         self,
-        size: Optional[int] = None,
-        call_args: Optional[FakeTransferFutureCallArgs] = None,
-        transfer_id: Optional[str] = None,
+        size: int | None = None,
+        call_args: FakeTransferFutureCallArgs | None = None,
+        transfer_id: str | None = None,
     ) -> None:
         """Instantiate class."""
         self.size = size
@@ -35,8 +35,8 @@ class FakeTransferFuture:
 
     def __init__(
         self,
-        result: Optional[str] = None,
-        exception: Optional[Exception] = None,
+        result: str | None = None,
+        exception: Exception | None = None,
         meta: FakeTransferFutureMeta = None,
     ) -> None:
         """Instantiate class."""
@@ -44,7 +44,7 @@ def __init__(
         self._exception = exception
         self.meta = meta or FakeTransferFutureMeta()
 
-    def result(self) -> Optional[str]:
+    def result(self) -> str | None:
         """Return result."""
         if self._exception:
             raise self._exception
diff --git a/tests/unit/core/providers/aws/s3/_helpers/test_results.py b/tests/unit/core/providers/aws/s3/_helpers/test_results.py
index 2ff84fbf8..0a4b5ff15 100644
--- a/tests/unit/core/providers/aws/s3/_helpers/test_results.py
+++ b/tests/unit/core/providers/aws/s3/_helpers/test_results.py
@@ -6,7 +6,7 @@
 from concurrent.futures import CancelledError
 from io import StringIO
 from queue import Queue
-from typing import TYPE_CHECKING, Any, ClassVar, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 from unittest.mock import Mock
 
 import pytest
@@ -14,7 +14,7 @@
 
 from runway._logging import LogLevels
 from runway.core.providers.aws.s3._helpers.results import (
-    AnyResult,
+    AnyResultType,
     BaseResultHandler,
     BaseResultSubscriber,
     CommandResult,
@@ -92,7 +92,7 @@ class BaseResultSubscriberTest:
     """Base class for result submitter test classes."""
 
     bucket: ClassVar[str] = "test-bucket"
-    dest: Optional[str]
+    dest: str | None
     failure_future: TransferFuture
     filename: ClassVar[str] = "test.txt"
     future: TransferFuture
@@ -110,8 +110,8 @@ def setup_method(self) -> None:
 
     def set_ref_transfer_futures(self) -> None:
         """Set reference transfer futures."""
-        self.future = self.get_success_transfer_future("foo")  # type: ignore
-        self.failure_future = self.get_failed_transfer_future(self.ref_exception)  # type: ignore
+        self.future = self.get_success_transfer_future("foo")
+        self.failure_future = self.get_failed_transfer_future(self.ref_exception)
 
     def get_success_transfer_future(self, result: str) -> TransferFuture:
         """Create a success transfer future."""
@@ -122,7 +122,7 @@ def get_failed_transfer_future(self, exception: Exception) -> TransferFuture:
         return self._get_transfer_future(exception=exception)  # type: ignore
 
     def _get_transfer_future(
-        self, result: Optional[Any] = None, exception: Optional[Exception] = None
+        self, result: Any | None = None, exception: Exception | None = None
     ) -> FakeTransferFuture:
         call_args = self._get_transfer_future_call_args()
         meta = FakeTransferFutureMeta(size=self.size, call_args=call_args)
@@ -131,7 +131,7 @@ def _get_transfer_future(
     def _get_transfer_future_call_args(self) -> FakeTransferFutureCallArgs:
         return FakeTransferFutureCallArgs(fileobj=self.filename, key=self.key, bucket=self.bucket)
 
-    def get_queued_result(self) -> AnyResult:
+    def get_queued_result(self) -> AnyResultType:
         """Get queued result."""
         return self.result_queue.get(block=False)
 
@@ -1233,7 +1233,7 @@ def setup_method(self) -> None:
     def test_get_ongoing_dict_key(self) -> None:
         """Test _get_ongoing_dict_key."""
         with pytest.raises(TypeError):
-            self.result_recorder._get_ongoing_dict_key(Mock())  # type: ignore
+            self.result_recorder._get_ongoing_dict_key(Mock())
 
     def test_record_error_result(self) -> None:
         """Test _record_error_result."""
diff --git a/tests/unit/core/providers/aws/s3/_helpers/test_s3handler.py b/tests/unit/core/providers/aws/s3/_helpers/test_s3handler.py
index 0add05f28..ba438e388 100644
--- a/tests/unit/core/providers/aws/s3/_helpers/test_s3handler.py
+++ b/tests/unit/core/providers/aws/s3/_helpers/test_s3handler.py
@@ -4,7 +4,7 @@
 
 from pathlib import Path
 from queue import Queue
-from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Optional, cast
+from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast
 from unittest.mock import MagicMock, Mock
 
 import pytest
@@ -132,7 +132,7 @@ def test_can_submit(self) -> None:
             ("s3://test", "s3://test"),
         ],
     )
-    def test_format_s3_path(self, expected: Optional[str], path: Optional[AnyPath]) -> None:
+    def test_format_s3_path(self, expected: str | None, path: AnyPath | None) -> None:
         """Test _format_s3_path."""
         assert (
             BaseTransferRequestSubmitter(
@@ -434,11 +434,9 @@ def setup_method(self) -> None:
 
     def assert_no_downloads_happened(self) -> None:
         """Assert not downloads."""
-        assert len(self.transfer_manager.download.call_args_list) == 0  # type: ignore
+        assert len(self.transfer_manager.download.call_args_list) == 0
 
-    def create_file_info(
-        self, key: str, response_data: Optional[dict[str, Any]] = None
-    ) -> FileInfo:
+    def create_file_info(self, key: str, response_data: dict[str, Any] | None = None) -> FileInfo:
         """Create FileInfo."""
         kwargs: dict[str, Any] = {
             "src": self.bucket + "/" + key,
diff --git a/tests/unit/core/providers/aws/s3/_helpers/test_utils.py b/tests/unit/core/providers/aws/s3/_helpers/test_utils.py
index 1e1e7c918..3020ca911 100644
--- a/tests/unit/core/providers/aws/s3/_helpers/test_utils.py
+++ b/tests/unit/core/providers/aws/s3/_helpers/test_utils.py
@@ -12,7 +12,7 @@
 from io import BytesIO
 from pathlib import Path
 from queue import Queue
-from typing import TYPE_CHECKING, Any, ClassVar, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
 from unittest.mock import Mock, PropertyMock, sentinel
 
 import boto3
@@ -917,7 +917,7 @@ def test_guess_content_type_handle_unicode_decode_error(
         (1024**7, None),
     ],
 )
-def test_human_readable_size(expected: Optional[str], value: float) -> None:
+def test_human_readable_size(expected: str | None, value: float) -> None:
     """Test human_readable_size."""
     assert human_readable_size(value) == expected
 
@@ -965,7 +965,7 @@ def test_relative_path_handle_value_error(mocker: MockerFixture, tmp_path: Path)
     [("/tmp/foo/bar", "/tmp/foo", f".{os.sep}bar"), (None, "/foo", None)],
 )
 def test_relative_path_posix(
-    expected: Optional[str], filename: Optional[str], mocker: MockerFixture, start: str
+    expected: str | None, filename: str | None, mocker: MockerFixture, start: str
 ) -> None:
     """Test relative_path."""
     mocker.patch("os.path.relpath", posixpath.relpath)
@@ -978,7 +978,7 @@ def test_relative_path_posix(
     [(None, "/foo", None), (r"C:\tmp\foo\bar", r"C:\tmp\foo", f".{os.sep}bar")],
 )
 def test_relative_path_windows(
-    expected: Optional[str], filename: Optional[str], mocker: MockerFixture, start: str
+    expected: str | None, filename: str | None, mocker: MockerFixture, start: str
 ) -> None:
     """Test relative_path."""
     mocker.patch("os.path.relpath", ntpath.relpath)
diff --git a/tests/unit/core/providers/aws/s3/test_bucket.py b/tests/unit/core/providers/aws/s3/test__bucket.py
similarity index 99%
rename from tests/unit/core/providers/aws/s3/test_bucket.py
rename to tests/unit/core/providers/aws/s3/test__bucket.py
index 8ddc70828..d64e9ecd9 100644
--- a/tests/unit/core/providers/aws/s3/test_bucket.py
+++ b/tests/unit/core/providers/aws/s3/test__bucket.py
@@ -1,6 +1,5 @@
 """Test runway.core.providers.aws.s3._bucket."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/core/providers/aws/s3/test_sync_handler.py b/tests/unit/core/providers/aws/s3/test__sync_handler.py
similarity index 100%
rename from tests/unit/core/providers/aws/s3/test_sync_handler.py
rename to tests/unit/core/providers/aws/s3/test__sync_handler.py
diff --git a/tests/unit/core/providers/aws/test_account.py b/tests/unit/core/providers/aws/test__account.py
similarity index 99%
rename from tests/unit/core/providers/aws/test_account.py
rename to tests/unit/core/providers/aws/test__account.py
index 06ba2516f..78ba55c05 100644
--- a/tests/unit/core/providers/aws/test_account.py
+++ b/tests/unit/core/providers/aws/test__account.py
@@ -1,6 +1,5 @@
 """Test runway.core.providers.aws._account."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/core/providers/aws/test_assume_role.py b/tests/unit/core/providers/aws/test__assume_role.py
similarity index 99%
rename from tests/unit/core/providers/aws/test_assume_role.py
rename to tests/unit/core/providers/aws/test__assume_role.py
index 75ab2e872..6dfc75657 100644
--- a/tests/unit/core/providers/aws/test_assume_role.py
+++ b/tests/unit/core/providers/aws/test__assume_role.py
@@ -1,6 +1,5 @@
 """Test runway.core.providers.aws._assume_role."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/core/providers/aws/test_response.py b/tests/unit/core/providers/aws/test__response.py
similarity index 99%
rename from tests/unit/core/providers/aws/test_response.py
rename to tests/unit/core/providers/aws/test__response.py
index 1c1397e18..e916da694 100644
--- a/tests/unit/core/providers/aws/test_response.py
+++ b/tests/unit/core/providers/aws/test__response.py
@@ -1,6 +1,5 @@
 """Test runway.core.providers.aws._response."""
 
-# pyright: basic
 from __future__ import annotations
 
 from runway.core.providers.aws import BaseResponse, ResponseError, ResponseMetadata
diff --git a/tests/unit/core/test_core.py b/tests/unit/core/test_core.py
index 79cbb9771..6139beff2 100644
--- a/tests/unit/core/test_core.py
+++ b/tests/unit/core/test_core.py
@@ -1,6 +1,5 @@
 """Test runway.core."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/dependency_managers/test__pip.py b/tests/unit/dependency_managers/test__pip.py
index cd41e8ae6..48c7017c8 100644
--- a/tests/unit/dependency_managers/test__pip.py
+++ b/tests/unit/dependency_managers/test__pip.py
@@ -5,7 +5,7 @@
 import logging
 import subprocess
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Union
+from typing import TYPE_CHECKING, Any
 from unittest.mock import Mock
 
 import pytest
@@ -48,7 +48,7 @@ def test_dir_is_project(self, expected: bool, kwargs: dict[str, str], tmp_path:
     def test_generate_command(
         self,
         caplog: pytest.LogCaptureFixture,
-        command: Union[list[str], str],
+        command: list[str] | str,
         mocker: MockerFixture,
     ) -> None:
         """Test generate_command."""
diff --git a/tests/unit/env_mgr/test_env_mgr.py b/tests/unit/env_mgr/test_env_mgr.py
index a0886cda0..5d5f08890 100644
--- a/tests/unit/env_mgr/test_env_mgr.py
+++ b/tests/unit/env_mgr/test_env_mgr.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -83,7 +83,7 @@ def test_bin(
         assert obj.bin == home / ".test-dir" / "versions" / "1.0.0" / "test-bin"
 
     @pytest.mark.parametrize("version", ["1.0.0", None])
-    def test_install(self, version: Optional[str]) -> None:
+    def test_install(self, version: str | None) -> None:
         """Test install."""
         with pytest.raises(NotImplementedError):
             assert EnvManager("", "").install(version)
diff --git a/tests/unit/env_mgr/test_kbenv.py b/tests/unit/env_mgr/test_kbenv.py
index 122ab59bb..5df3858db 100644
--- a/tests/unit/env_mgr/test_kbenv.py
+++ b/tests/unit/env_mgr/test_kbenv.py
@@ -1,10 +1,10 @@
 """Test runway.env_mgr.kbenv."""
 
-# pyright: basic, reportFunctionMemberAccess=none
+# pyright: reportFunctionMemberAccess=none
 from __future__ import annotations
 
 import re
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -73,7 +73,7 @@ def test_list_installed_none(self, mocker: MockerFixture, tmp_path: Path) -> Non
             ("v0.15.0-alpha.13", Version("v0.15.0-alpha.13")),
         ],
     )
-    def test_parse_version_string(self, provided: str, expected: Optional[Version]) -> None:
+    def test_parse_version_string(self, provided: str, expected: Version | None) -> None:
         """Test parse_version_string."""
         assert KBEnvManager.parse_version_string(provided) == expected
 
diff --git a/tests/unit/env_mgr/test_tfenv.py b/tests/unit/env_mgr/test_tfenv.py
index 3b45d1af4..b4e53bfb3 100644
--- a/tests/unit/env_mgr/test_tfenv.py
+++ b/tests/unit/env_mgr/test_tfenv.py
@@ -1,12 +1,12 @@
 """Test runway.env_mgr.tfenv."""
 
-# pyright: basic, reportFunctionMemberAccess=none
+# pyright: reportFunctionMemberAccess=none
 from __future__ import annotations
 
 import json
 import re
 import subprocess
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, call
 
 import hcl
@@ -192,7 +192,7 @@ def test_get_min_required(self, mocker: MockerFixture, tmp_path: Path) -> None:
     )
     def test_get_version_from_executable(
         self,
-        expected: Optional[Version],
+        expected: Version | None,
         fake_process: FakeProcess,
         output: str,
     ) -> None:
@@ -292,7 +292,7 @@ def test_list_installed_none(self, mocker: MockerFixture, tmp_path: Path) -> Non
             ("0.15.0-alpha13", Version("0.15.0-alpha13")),
         ],
     )
-    def test_parse_version_string(self, provided: str, expected: Optional[Version]) -> None:
+    def test_parse_version_string(self, provided: str, expected: Version | None) -> None:
         """Test parse_version_string."""
         assert TFEnvManager.parse_version_string(provided) == expected
 
diff --git a/tests/unit/lookups/handlers/test_base.py b/tests/unit/lookups/handlers/test_base.py
index e4c348289..6966df71d 100644
--- a/tests/unit/lookups/handlers/test_base.py
+++ b/tests/unit/lookups/handlers/test_base.py
@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import json
-from typing import Optional
 from unittest.mock import MagicMock
 
 import pytest
@@ -75,7 +74,7 @@ def test_format_results(self) -> None:
             ("undefined", "undefined"),
         ],
     )
-    def test_format_results_handle_none(self, value: str, expected: Optional[str]) -> None:
+    def test_format_results_handle_none(self, value: str, expected: str | None) -> None:
         """Test format_results."""
         assert LookupHandler.format_results(value) == expected
         if isinstance(expected, str):
@@ -117,7 +116,7 @@ def test_load_list(self) -> None:
     def test_parse(
         self,
         query: str,
-        raw_args: Optional[dict[str, str]],
+        raw_args: dict[str, str] | None,
         expected_args: dict[str, str],
     ) -> None:
         """Test parse."""
diff --git a/tests/unit/lookups/handlers/test_cfn.py b/tests/unit/lookups/handlers/test_cfn.py
index 274b2a589..ccc7cba99 100644
--- a/tests/unit/lookups/handlers/test_cfn.py
+++ b/tests/unit/lookups/handlers/test_cfn.py
@@ -1,12 +1,12 @@
 """Test runway.lookups.handlers.cfn."""
 
-# pyright: basic, reportFunctionMemberAccess=none
+# pyright: reportFunctionMemberAccess=none
 from __future__ import annotations
 
 import json
 import logging
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock
 
 import boto3
@@ -29,7 +29,7 @@
 def generate_describe_stacks_stack(
     stack_name: str,
     outputs: dict[str, str],
-    creation_time: Optional[datetime] = None,
+    creation_time: datetime | None = None,
     stack_status: str = "CREATE_COMPLETE",
 ) -> dict[str, Any]:
     """Generate describe stacks stack.
@@ -37,7 +37,7 @@ def generate_describe_stacks_stack(
     Args:
         stack_name: Name of the stack.
         outputs: Dictionary to be converted to stack outputs.
-        creation_time (Optional[datetime.datetime]): Stack creation time.
+        creation_time: Stack creation time.
         stack_status: Current stack status.
 
     Returns:
@@ -126,7 +126,7 @@ def test_handle(self, mocker: MockerFixture) -> None:
     def test_handle_exception(
         self,
         caplog: pytest.LogCaptureFixture,
-        default: Optional[str],
+        default: str | None,
         exception: Exception,
         mocker: MockerFixture,
     ) -> None:
@@ -182,7 +182,7 @@ def test_handle_exception(
     def test_handle_provider_exception(
         self,
         caplog: pytest.LogCaptureFixture,
-        default: Optional[str],
+        default: str | None,
         exception: Exception,
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
@@ -302,7 +302,7 @@ def test_should_use_provider_falsy(
         self,
         args: dict[str, Any],
         caplog: pytest.LogCaptureFixture,
-        provider: Optional[Provider],
+        provider: Provider | None,
     ) -> None:
         """Test should_use_provider with falsy cases."""
         caplog.set_level(logging.DEBUG, logger="runway.lookups.handlers.cfn")
@@ -322,7 +322,7 @@ def test_should_use_provider_truthy(
         self,
         args: dict[str, Any],
         caplog: pytest.LogCaptureFixture,
-        provider: Optional[Provider],
+        provider: Provider | None,
     ) -> None:
         """Test should_use_provider with truthy cases."""
         caplog.set_level(logging.DEBUG, logger="runway.lookups.handlers.cfn")
diff --git a/tests/unit/lookups/handlers/test_env.py b/tests/unit/lookups/handlers/test_env.py
index f0f7d054c..ab676219f 100644
--- a/tests/unit/lookups/handlers/test_env.py
+++ b/tests/unit/lookups/handlers/test_env.py
@@ -1,6 +1,5 @@
 """Tests for lookup handler for env."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/lookups/handlers/test_random_string.py b/tests/unit/lookups/handlers/test_random_string.py
index 222e99503..4bd7ed8af 100644
--- a/tests/unit/lookups/handlers/test_random_string.py
+++ b/tests/unit/lookups/handlers/test_random_string.py
@@ -95,7 +95,7 @@ class TestRandomStringLookup:
     )
     def test_calculate_char_set(self, args: object, expected: str) -> None:
         """Test calculate_char_set."""
-        assert RandomStringLookup.calculate_char_set(ArgsDataModel.parse_obj(args)) == expected
+        assert RandomStringLookup.calculate_char_set(ArgsDataModel.model_validate(args)) == expected
 
     @pytest.mark.parametrize(
         "args, value, expected",
@@ -124,7 +124,8 @@ def test_calculate_char_set(self, args: object, expected: str) -> None:
     def test_ensure_has_one_of(self, args: object, expected: bool, value: str) -> None:
         """Test ensure_has_one_of."""
         assert (
-            RandomStringLookup.ensure_has_one_of(ArgsDataModel.parse_obj(args), value) is expected
+            RandomStringLookup.ensure_has_one_of(ArgsDataModel.model_validate(args), value)
+            is expected
         )
 
     @pytest.mark.parametrize("length", [1, 3, 5, 7, 8, 9])
diff --git a/tests/unit/lookups/handlers/test_ssm.py b/tests/unit/lookups/handlers/test_ssm.py
index 4ef5616cb..576af1403 100644
--- a/tests/unit/lookups/handlers/test_ssm.py
+++ b/tests/unit/lookups/handlers/test_ssm.py
@@ -1,11 +1,10 @@
 """Test runway.lookups.handlers.ssm."""
 
-# pyright: basic
 from __future__ import annotations
 
 import json
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Optional, Union
+from typing import TYPE_CHECKING, Any
 
 import pytest
 import yaml
@@ -20,8 +19,9 @@
 def get_parameter_response(
     name: str,
     value: str,
+    *,
     value_type: str = "String",
-    label: Optional[str] = None,
+    label: str | None = None,
     version: int = 1,
 ) -> dict[str, Any]:
     """Generate a mock ssm.get_parameter response."""
@@ -40,7 +40,7 @@ def get_parameter_response(
     }
 
 
-def get_parameter_request(name: str, decrypt: bool = True) -> dict[str, Union[bool, str]]:
+def get_parameter_request(name: str, decrypt: bool = True) -> dict[str, bool | str]:
     """Generate the expected request parameters for ssm.get_parameter."""
     return {"Name": name, "WithDecryption": decrypt}
 
@@ -48,7 +48,7 @@ def get_parameter_request(name: str, decrypt: bool = True) -> dict[str, Union[bo
 class TestSsmLookup:
     """Test runway.lookups.handlers.ssm.SsmLookup."""
 
-    def test_basic(
+    def test_handle_basic(
         self, cfngin_context: MockCfnginContext, runway_context: MockRunwayContext
     ) -> None:
         """Test resolution of a basic lookup."""
@@ -76,7 +76,7 @@ def test_basic(
         cfn_stub.assert_no_pending_responses()
         rw_stub.assert_no_pending_responses()
 
-    def test_default(self, runway_context: MockRunwayContext) -> None:
+    def test_handle_default(self, runway_context: MockRunwayContext) -> None:
         """Test resolution of a default value."""
         name = "/test/param"
         value = "test value"
@@ -103,7 +103,7 @@ def test_default(self, runway_context: MockRunwayContext) -> None:
             assert var.value == value
             stub.assert_no_pending_responses()
 
-    def test_different_region(self, runway_context: MockRunwayContext) -> None:
+    def test_handle_different_region(self, runway_context: MockRunwayContext) -> None:
         """Test Lookup in region other than that set in Context."""
         name = "/test/param"
         value = "test value"
@@ -121,7 +121,7 @@ def test_different_region(self, runway_context: MockRunwayContext) -> None:
             assert var.value == value
             stub.assert_no_pending_responses()
 
-    def test_loaded_value(self, runway_context: MockRunwayContext) -> None:
+    def test_handle_loaded_value(self, runway_context: MockRunwayContext) -> None:
         """Test resolution of a JSON value."""
         name = "/test/param"
         raw_value = {
@@ -171,7 +171,7 @@ def test_loaded_value(self, runway_context: MockRunwayContext) -> None:
                     assert var.value == test["expected"]
                     stub.assert_no_pending_responses()
 
-    def test_not_found(self, runway_context: MockRunwayContext) -> None:
+    def test_handle_not_found(self, runway_context: MockRunwayContext) -> None:
         """Test raises ParameterNotFound."""
         name = "/test/param"
         stubber = runway_context.add_stubber("ssm")
@@ -188,3 +188,35 @@ def test_not_found(self, runway_context: MockRunwayContext) -> None:
 
         assert "ParameterNotFound" in str(err.value.__cause__)
         stub.assert_no_pending_responses()
+
+    def test_handle_no_value(self, runway_context: MockRunwayContext) -> None:
+        """Test handle no ``Value`` in response."""
+        name = "/test/param"
+        value = "foo"
+        stubber = runway_context.add_stubber("ssm")
+        var = Variable("test_var", f"${{ssm {name}}}", variable_type="runway")
+        response = get_parameter_response(name, value)
+        response["Parameter"].pop("Value", None)
+        stubber.add_response("get_parameter", response, get_parameter_request(name))
+
+        with stubber:
+            var.resolve(context=runway_context)
+            assert var.value is None
+        stubber.assert_no_pending_responses()
+
+    def test_handle_string_list(self, runway_context: MockRunwayContext) -> None:
+        """Test handle ``StringList`` returned as list."""
+        name = "/test/param"
+        value = ["foo", "bar"]
+        stubber = runway_context.add_stubber("ssm")
+        var = Variable("test_var", f"${{ssm {name}}}", variable_type="runway")
+        stubber.add_response(
+            "get_parameter",
+            get_parameter_response(name, ",".join(value), value_type="StringList"),
+            get_parameter_request(name),
+        )
+
+        with stubber:
+            var.resolve(context=runway_context)
+            assert var.value == value
+        stubber.assert_no_pending_responses()
diff --git a/tests/unit/lookups/handlers/test_var.py b/tests/unit/lookups/handlers/test_var.py
index e15adc7b6..bccf08e38 100644
--- a/tests/unit/lookups/handlers/test_var.py
+++ b/tests/unit/lookups/handlers/test_var.py
@@ -1,6 +1,5 @@
 """Tests for lookup handler for var."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
diff --git a/tests/unit/lookups/test_registry.py b/tests/unit/lookups/test_registry.py
index 9a53e7353..416f8a54a 100644
--- a/tests/unit/lookups/test_registry.py
+++ b/tests/unit/lookups/test_registry.py
@@ -1,6 +1,5 @@
 """Tests for lookup registry and common lookup functionality."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
diff --git a/tests/unit/mock_docker/fake_api.py b/tests/unit/mock_docker/fake_api.py
index 9b997ea0e..173d80ae9 100644
--- a/tests/unit/mock_docker/fake_api.py
+++ b/tests/unit/mock_docker/fake_api.py
@@ -2,7 +2,9 @@
 
 # cspell:disable
 # ruff: noqa: D103
-from typing import Any, Callable, Union
+from __future__ import annotations
+
+from typing import Any, Callable
 
 from docker import constants
 
@@ -523,7 +525,7 @@ def post_fake_network_disconnect() -> tuple[int, Any]:
 if constants.IS_WINDOWS_PLATFORM:
     prefix = "http+docker://localnpipe"
 
-fake_responses: dict[Union[str, tuple[str, str]], Callable[..., tuple[int, Any]]] = {
+fake_responses: dict[str | tuple[str, str], Callable[..., tuple[int, Any]]] = {
     f"{prefix}/version": get_fake_version,
     f"{prefix}/{CURRENT_VERSION}/version": get_fake_version,
     f"{prefix}/{CURRENT_VERSION}/info": get_fake_info,
diff --git a/tests/unit/mock_docker/fake_api_client.py b/tests/unit/mock_docker/fake_api_client.py
index 1d106f53e..bfb1b7bae 100644
--- a/tests/unit/mock_docker/fake_api_client.py
+++ b/tests/unit/mock_docker/fake_api_client.py
@@ -1,7 +1,9 @@
 """Fake Docker API client."""
 
+from __future__ import annotations
+
 import copy
-from typing import Any, Optional
+from typing import Any
 from unittest import mock
 
 import docker
@@ -20,7 +22,7 @@ def _mock_call(self, *args: Any, **kwargs: Any) -> Any:
         return ret  # type: ignore
 
 
-def make_fake_api_client(overrides: Optional[dict[str, Any]] = None) -> CopyReturnMagicMock:
+def make_fake_api_client(overrides: dict[str, Any] | None = None) -> CopyReturnMagicMock:
     """Return non-complete fake APIClient.
 
     This returns most of the default cases correctly, but most arguments that
@@ -56,7 +58,7 @@ def make_fake_api_client(overrides: Optional[dict[str, Any]] = None) -> CopyRetu
     return mock_client
 
 
-def make_fake_client(overrides: Optional[dict[str, Any]] = None) -> docker.DockerClient:
+def make_fake_client(overrides: dict[str, Any] | None = None) -> docker.DockerClient:
     """Return a Client with a fake APIClient."""
     client = docker.DockerClient(version=DEFAULT_DOCKER_API_VERSION)
     client.api = make_fake_api_client(overrides)
diff --git a/tests/unit/module/staticsite/options/test_components.py b/tests/unit/module/staticsite/options/test__components.py
similarity index 73%
rename from tests/unit/module/staticsite/options/test_components.py
rename to tests/unit/module/staticsite/options/test__components.py
index 77ee203bc..fd866bc65 100644
--- a/tests/unit/module/staticsite/options/test_components.py
+++ b/tests/unit/module/staticsite/options/test__components.py
@@ -1,15 +1,14 @@
-"""Test runway.module.staticsite.options.components."""
+"""Test runway.module.staticsite.options._components."""
 
-# pyright: basic
 from __future__ import annotations
 
-from runway.module.staticsite.options.components import StaticSiteOptions
-from runway.module.staticsite.options.models import (
+from runway.module.staticsite.options._components import StaticSiteOptions
+from runway.module.staticsite.options._models import (
     RunwayStaticSiteModuleOptionsDataModel,
     RunwayStaticSitePreBuildStepDataModel,
 )
 
-MODULE = "runway.module.staticsite.options.components"
+MODULE = "runway.module.staticsite.options._components"
 
 
 class TestStaticSiteOptions:
@@ -34,4 +33,6 @@ def test_parse_obj(self) -> None:
         """Test parse_obj."""
         obj = StaticSiteOptions.parse_obj({})
         assert isinstance(obj.data, RunwayStaticSiteModuleOptionsDataModel)
-        assert obj.data.dict(exclude_defaults=True, exclude_none=True, exclude_unset=True) == {}
+        assert (
+            obj.data.model_dump(exclude_defaults=True, exclude_none=True, exclude_unset=True) == {}
+        )
diff --git a/tests/unit/module/staticsite/options/test_models.py b/tests/unit/module/staticsite/options/test__models.py
similarity index 92%
rename from tests/unit/module/staticsite/options/test_models.py
rename to tests/unit/module/staticsite/options/test__models.py
index 62372f679..223455518 100644
--- a/tests/unit/module/staticsite/options/test_models.py
+++ b/tests/unit/module/staticsite/options/test__models.py
@@ -1,15 +1,14 @@
 """Test runway.module.staticsite.options.models."""
 
-# pyright: basic
 from __future__ import annotations
 
 from pathlib import Path
-from typing import Any, Optional, cast
+from typing import Any, cast
 
 import pytest
 from pydantic import ValidationError
 
-from runway.module.staticsite.options.models import (
+from runway.module.staticsite.options._models import (
     RunwayStaticSiteExtraFileDataModel,
     RunwayStaticSiteModuleOptionsDataModel,
     RunwayStaticSitePreBuildStepDataModel,
@@ -32,7 +31,7 @@ class TestRunwayStaticSiteExtraFileDataModel:
             ("test", None),
         ],
     )
-    def test_autofill_content_type(self, expected: Optional[str], name: str) -> None:
+    def test_autofill_content_type(self, expected: str | None, name: str) -> None:
         """Test _autofill_content_type."""
         assert (
             RunwayStaticSiteExtraFileDataModel(content="test", name=name).content_type == expected
@@ -63,7 +62,7 @@ def test_init_content_and_file(self, tmp_path: Path) -> None:
     def test_init_content(self) -> None:
         """Test init content."""
         data = {"content_type": "test-data", "content": "content", "name": "test"}
-        obj = RunwayStaticSiteExtraFileDataModel(**data)
+        obj = RunwayStaticSiteExtraFileDataModel.model_validate(data)
         assert obj.content_type == data["content_type"]
         assert obj.content == data["content"]
         assert not obj.file
@@ -72,7 +71,7 @@ def test_init_content(self) -> None:
     def test_init_file(self, tmp_path: Path) -> None:
         """Test init file."""
         data = {"content_type": "test-data", "file": tmp_path, "name": "test"}
-        obj = RunwayStaticSiteExtraFileDataModel.parse_obj(data)
+        obj = RunwayStaticSiteExtraFileDataModel.model_validate(data)
         assert obj.content_type == data["content_type"]
         assert not obj.content
         assert obj.file == data["file"]
@@ -111,7 +110,7 @@ def test_init_default(self) -> None:
     def test_init_extra(self) -> None:
         """Test init extra."""
         obj = RunwayStaticSiteModuleOptionsDataModel(invalid="val")  # type: ignore
-        assert "invalid" not in obj.dict()
+        assert "invalid" not in obj.model_dump()
 
     def test_init(self) -> None:
         """Test init."""
@@ -122,7 +121,7 @@ def test_init(self) -> None:
             "pre_build_steps": [{"command": "runway --help"}],
             "source_hashing": {"enabled": False},
         }
-        obj = RunwayStaticSiteModuleOptionsDataModel(**data)
+        obj = RunwayStaticSiteModuleOptionsDataModel.model_validate(data)
         assert obj.build_output == data["build_output"]
         assert obj.build_steps == data["build_steps"]
         assert obj.extra_files == [
@@ -179,7 +178,7 @@ def test_init_default(self) -> None:
     def test_init_extra(self) -> None:
         """Test init extra."""
         with pytest.raises(ValidationError):
-            RunwayStaticSiteSourceHashingDataModel.parse_obj({"invalid": "test"})
+            RunwayStaticSiteSourceHashingDataModel.model_validate({"invalid": "test"})
 
     def test_init(self, tmp_path: Path) -> None:
         """Test init."""
@@ -188,7 +187,7 @@ def test_init(self, tmp_path: Path) -> None:
             "enabled": False,
             "parameter": "test",
         }
-        obj = RunwayStaticSiteSourceHashingDataModel(**data)
+        obj = RunwayStaticSiteSourceHashingDataModel.model_validate(data)
         assert obj.directories == [
             RunwayStaticSiteSourceHashingDirectoryDataModel(
                 **data["directories"][0]  # type: ignore
@@ -225,6 +224,6 @@ def test_init_required(self) -> None:
     def test_init(self, tmp_path: Path) -> None:
         """Test init."""
         data = {"exclusions": ["**/*.md"], "path": tmp_path}
-        obj = RunwayStaticSiteSourceHashingDirectoryDataModel.parse_obj(data)
+        obj = RunwayStaticSiteSourceHashingDirectoryDataModel.model_validate(data)
         assert obj.exclusions == data["exclusions"]
         assert obj.path == data["path"]
diff --git a/tests/unit/module/staticsite/parameters/test_models.py b/tests/unit/module/staticsite/parameters/test__models.py
similarity index 99%
rename from tests/unit/module/staticsite/parameters/test_models.py
rename to tests/unit/module/staticsite/parameters/test__models.py
index 712033827..7eb03c537 100644
--- a/tests/unit/module/staticsite/parameters/test_models.py
+++ b/tests/unit/module/staticsite/parameters/test__models.py
@@ -1,12 +1,11 @@
 """Test runway.module.staticsite.parameters.models."""
 
-# pyright: basic
 from typing import Any, cast
 
 import pytest
 from pydantic import ValidationError
 
-from runway.module.staticsite.parameters.models import (
+from runway.module.staticsite.parameters._models import (
     RunwayStaticSiteCustomErrorResponseDataModel,
     RunwayStaticSiteLambdaFunctionAssociationDataModel,
     RunwayStaticSiteModuleParametersDataModel,
diff --git a/tests/unit/module/staticsite/test_handler.py b/tests/unit/module/staticsite/test_handler.py
index 520e4de8d..059cea653 100644
--- a/tests/unit/module/staticsite/test_handler.py
+++ b/tests/unit/module/staticsite/test_handler.py
@@ -11,10 +11,8 @@
 import pytest
 
 from runway.module.staticsite.handler import StaticSite
-from runway.module.staticsite.options.components import StaticSiteOptions
-from runway.module.staticsite.parameters.models import (
-    RunwayStaticSiteModuleParametersDataModel,
-)
+from runway.module.staticsite.options import StaticSiteOptions
+from runway.module.staticsite.parameters import RunwayStaticSiteModuleParametersDataModel
 
 if TYPE_CHECKING:
     from pathlib import Path
@@ -48,7 +46,7 @@ def test___init__(self, runway_context: RunwayContext, tmp_path: Path) -> None:
         assert isinstance(obj.options, StaticSiteOptions)
         assert obj.options == StaticSiteOptions.parse_obj({"build_output": "./dist"})
         assert isinstance(obj.parameters, RunwayStaticSiteModuleParametersDataModel)
-        assert obj.parameters == RunwayStaticSiteModuleParametersDataModel.parse_obj(
+        assert obj.parameters == RunwayStaticSiteModuleParametersDataModel.model_validate(
             {"namespace": "test"}
         )
         assert obj.path == tmp_path
diff --git a/tests/unit/module/test_cdk.py b/tests/unit/module/test_cdk.py
index 7d5cfc6ac..c18bfb263 100644
--- a/tests/unit/module/test_cdk.py
+++ b/tests/unit/module/test_cdk.py
@@ -4,7 +4,7 @@
 
 import logging
 from subprocess import CalledProcessError
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 from unittest.mock import Mock, call
 
 import pytest
@@ -381,7 +381,7 @@ def test_destroy(
     )
     def test_gen_cmd(
         self,
-        args_list: Optional[list[str]],
+        args_list: list[str] | None,
         command: CdkCommandTypeDef,
         env_ci: bool,
         expected: list[str],
@@ -625,4 +625,4 @@ def test_parse_obj(self) -> None:
         assert isinstance(obj.data, RunwayCdkModuleOptionsDataModel)
         assert obj.data.build_steps == config["build_steps"]
         assert obj.data.skip_npm_ci == config["skip_npm_ci"]
-        assert "key" not in obj.data.dict()
+        assert "key" not in obj.data.model_dump()
diff --git a/tests/unit/module/test_cloudformation.py b/tests/unit/module/test_cloudformation.py
index 575e7eeeb..ceead2e96 100644
--- a/tests/unit/module/test_cloudformation.py
+++ b/tests/unit/module/test_cloudformation.py
@@ -1,6 +1,5 @@
 """Test runway.module.cloudformation."""
 
-# pyright: basic
 from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
diff --git a/tests/unit/module/test_k8s.py b/tests/unit/module/test_k8s.py
index 20bad3a1f..687993022 100644
--- a/tests/unit/module/test_k8s.py
+++ b/tests/unit/module/test_k8s.py
@@ -1,11 +1,10 @@
 """Test runway.module.k8s."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
 from subprocess import CalledProcessError
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
 import pytest
 import yaml
@@ -85,7 +84,7 @@ def test_destroy(
     )
     def test_gen_cmd(
         self,
-        args_list: Optional[list[str]],
+        args_list: list[str] | None,
         command: KubectlCommandTypeDef,
         expected: list[str],
         mocker: MockerFixture,
diff --git a/tests/unit/module/test_serverless.py b/tests/unit/module/test_serverless.py
index 4929f63ff..0afc21f90 100644
--- a/tests/unit/module/test_serverless.py
+++ b/tests/unit/module/test_serverless.py
@@ -1,11 +1,11 @@
 """Test runway.module.serverless."""
 
-# pyright: basic, reportFunctionMemberAccess=none
+# pyright: reportFunctionMemberAccess=none
 from __future__ import annotations
 
 import logging
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, cast
 from unittest.mock import ANY, MagicMock, Mock, call
 
 import pytest
@@ -425,7 +425,7 @@ def test_skip(
     def test_sls_deploy(
         self,
         mocker: MockerFixture,
-        package: Optional[str],
+        package: str | None,
         runway_context: MockRunwayContext,
         skip_install: bool,
         tmp_path: Path,
@@ -455,7 +455,7 @@ def test_sls_deploy(
     def test_sls_package(
         self,
         mocker: MockerFixture,
-        output_path: Optional[AnyPathConstrained],
+        output_path: AnyPathConstrained | None,
         runway_context: MockRunwayContext,
         skip_install: bool,
         tmp_path: Path,
@@ -486,7 +486,7 @@ def test_sls_package(
     )
     def test_sls_print(
         self,
-        item_path: Optional[str],
+        item_path: str | None,
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
         skip_install: bool,
@@ -622,7 +622,7 @@ def test_source_hash(
         self,
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
-        service: Union[dict[str, Any], str],
+        service: dict[str, Any] | str,
         service_name: str,
         tmp_path: Path,
     ) -> None:
@@ -652,7 +652,7 @@ def test_source_hash_individually(
         self,
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
-        service: Union[dict[str, Any], str],
+        service: dict[str, Any] | str,
         tmp_path: Path,
     ) -> None:
         """Test source_hash."""
diff --git a/tests/unit/module/test_terraform.py b/tests/unit/module/test_terraform.py
index d61ec5e8e..60dc18c0f 100644
--- a/tests/unit/module/test_terraform.py
+++ b/tests/unit/module/test_terraform.py
@@ -6,7 +6,7 @@
 import json
 import logging
 import subprocess
-from typing import TYPE_CHECKING, Any, Optional, Union
+from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, Mock
 
 import pytest
@@ -192,8 +192,8 @@ def test_current_workspace(
     )
     def test_env_file(
         self,
-        filename: Union[list[str], str],
-        expected: Optional[str],
+        filename: list[str] | str,
+        expected: str | None,
         runway_context: MockRunwayContext,
         tmp_path: Path,
     ) -> None:
@@ -300,8 +300,8 @@ def test_execute(  # noqa: PLR0915
     )
     def test_gen_command(
         self,
-        command: Union[list[str], str],
-        args_list: Optional[list[str]],
+        command: list[str] | str,
+        args_list: list[str] | None,
         expected: list[str],
         mocker: MockerFixture,
         runway_context: MockRunwayContext,
@@ -329,7 +329,7 @@ def test_handle_backend_no_handler(
         """Test handle_backend with no handler."""
         caplog.set_level(LogLevels.DEBUG, logger=MODULE)
         mock_get_full_configuration = MagicMock(return_value={})
-        backend: dict[str, Union[dict[str, Any], str]] = {
+        backend: dict[str, dict[str, Any] | str] = {
             "type": "unsupported",
             "config": {},
         }
@@ -427,7 +427,7 @@ def test_handle_backend_remote_undetermined(
         caplog.set_level(LogLevels.WARNING, logger=MODULE)
         monkeypatch.delenv("TF_WORKSPACE", raising=False)
         mock_get_full_configuration = MagicMock(return_value={})
-        backend: dict[str, Union[dict[str, Any], str]] = {
+        backend: dict[str, dict[str, Any] | str] = {
             "type": "remote",
             "config": {},
         }
@@ -642,7 +642,7 @@ def test_terraform_init(
             Terraform, "gen_command", return_value=["mock_gen_command"]
         )
         mock_run_command = mocker.patch(f"{MODULE}.run_module_command")
-        options: dict[str, Union[dict[str, Any], str]] = {
+        options: dict[str, dict[str, Any] | str] = {
             "args": {"init": ["init_arg"]},
             "terraform_backend_config": {"bucket": "name"},
         }
@@ -1038,7 +1038,7 @@ def test_gen_backend_filenames(self) -> None:
         ],
     )
     def test_get_backend_file(
-        self, tmp_path: Path, filename: Union[list[str], str], expected: Optional[str]
+        self, tmp_path: Path, filename: list[str] | str, expected: str | None
     ) -> None:
         """Test get_backend_file."""
         if isinstance(filename, list):
diff --git a/tests/unit/sources/test_git.py b/tests/unit/sources/test_git.py
index 26adc0917..ae96aafbc 100644
--- a/tests/unit/sources/test_git.py
+++ b/tests/unit/sources/test_git.py
@@ -1,6 +1,5 @@
 """Tests for the Source type object."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/sources/test_source.py b/tests/unit/sources/test_source.py
index 0bfc9cce8..46b20504d 100644
--- a/tests/unit/sources/test_source.py
+++ b/tests/unit/sources/test_source.py
@@ -1,6 +1,5 @@
 """Tests for the Source type object."""
 
-# pyright: basic
 from __future__ import annotations
 
 import logging
diff --git a/tests/unit/test__json_encoder.py b/tests/unit/test__json_encoder.py
new file mode 100644
index 000000000..dd479f578
--- /dev/null
+++ b/tests/unit/test__json_encoder.py
@@ -0,0 +1,40 @@
+"""Test runway.utils._json_encoder."""
+
+from __future__ import annotations
+
+import datetime
+from decimal import Decimal
+from pathlib import Path
+from typing import Any
+
+import pytest
+from packaging.specifiers import SpecifierSet
+
+from runway.config.models.runway import RunwayAssumeRoleDefinitionModel
+from runway.utils import JsonEncoder
+
+
+class TestJsonEncoder:
+    """Test JsonEncoder."""
+
+    @pytest.mark.parametrize(
+        "provided, expected",
+        [
+            (("foo", "bar"), list),
+            (Decimal("1.1"), float),
+            (Path.cwd() / ".runway", str),
+            (RunwayAssumeRoleDefinitionModel(), dict),
+            (SpecifierSet("==1.0"), str),
+            (datetime.datetime.now(), str),
+            ({"foo"}, list),
+        ],
+    )
+    def test_supported_types(self, provided: Any, expected: type) -> None:
+        """Test encoding of supported data types."""
+        assert isinstance(JsonEncoder().default(provided), expected)
+
+    @pytest.mark.parametrize("provided", [(None)])
+    def test_unsupported_types(self, provided: Any) -> None:
+        """Test encoding of unsupported data types."""
+        with pytest.raises(TypeError):
+            assert not JsonEncoder().default(provided)
diff --git a/tests/unit/test_mixins.py b/tests/unit/test_mixins.py
index eaa974a08..1ea59e3ea 100644
--- a/tests/unit/test_mixins.py
+++ b/tests/unit/test_mixins.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import subprocess
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any
 from unittest.mock import Mock
 
 import pytest
@@ -34,7 +34,7 @@ def __init__(self, context: CfnginContext, cwd: Path) -> None:
 
     @pytest.mark.parametrize("env", [None, {"foo": "bar"}])
     def test__run_command(
-        self, env: Optional[dict[str, str]], mocker: MockerFixture, tmp_path: Path
+        self, env: dict[str, str] | None, mocker: MockerFixture, tmp_path: Path
     ) -> None:
         """Test _run_command."""
         ctx_env = {"foo": "bar", "bar": "foo"}
@@ -76,7 +76,7 @@ def test__run_command_no_suppress_output(self, mocker: MockerFixture, tmp_path:
             ("--", "foo-bar", "--foo-bar"),
         ],
     )
-    def test_convert_to_cli_arg(self, expected: str, prefix: Optional[str], provided: str) -> None:
+    def test_convert_to_cli_arg(self, expected: str, prefix: str | None, provided: str) -> None:
         """Test convert_to_cli_arg."""
         if prefix:
             assert self.Kls.convert_to_cli_arg(provided, prefix=prefix) == expected
diff --git a/tests/unit/test_variables.py b/tests/unit/test_variables.py
index 6aea2af7f..695dc727e 100644
--- a/tests/unit/test_variables.py
+++ b/tests/unit/test_variables.py
@@ -1,9 +1,8 @@
 """Tests for runway.variables."""
 
-# pyright: basic
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, ClassVar, Union
+from typing import TYPE_CHECKING, Any, ClassVar
 from unittest.mock import MagicMock, call
 
 import pytest
@@ -39,11 +38,17 @@
     from .factories import MockCfnginContext
 
 
+class ExampleModel(BaseModel):
+    """Example model used for testing."""
+
+    test: Any = "val"
+
+
 class MockLookupHandler(LookupHandler):
     """Mock lookup handler."""
 
     return_value: ClassVar[Any] = "resolved"
-    side_effect: ClassVar[Union[Any, list[Any]]] = None
+    side_effect: ClassVar[Any | list[Any]] = None
 
     @classmethod
     def handle(
@@ -290,7 +295,7 @@ def test_parse_obj_literal_str(self) -> None:
 
     def test_parse_obj_pydantic_model(self) -> None:
         """Test parse_obj pydantic model."""
-        assert isinstance(VariableValue.parse_obj(BaseModel()), VariableValuePydanticModel)
+        assert isinstance(VariableValue.parse_obj(ExampleModel()), VariableValuePydanticModel)
 
     def test_repr(self) -> None:
         """Test __repr__."""
@@ -686,31 +691,31 @@ class TestVariableValueLiteral:
     """Test runway.variables.VariableValueLiteral."""
 
     @pytest.mark.parametrize("value", [False, True, 13, "test"])
-    def test_init(self, value: Union[int, str]) -> None:
+    def test_init(self, value: int | str) -> None:
         """Test __init__."""
         obj = VariableValueLiteral(value)  # type: ignore
         assert obj._data == value
 
     @pytest.mark.parametrize("value", [False, True, 13, "test"])
-    def test_iter(self, value: Union[int, str]) -> None:
+    def test_iter(self, value: int | str) -> None:
         """Test __iter__."""
         obj = VariableValueLiteral(value)  # type: ignore
         assert list(iter(obj)) == [obj]  # type: ignore
 
     @pytest.mark.parametrize("value", [False, True, 13, "test"])
-    def test_repr(self, value: Union[int, str]) -> None:
+    def test_repr(self, value: int | str) -> None:
         """Test __repr__."""
         obj = VariableValueLiteral(value)  # type: ignore
         assert repr(obj) == f"Literal[{value}]"  # type: ignore
 
     @pytest.mark.parametrize("value", [False, True, 13, "test"])
-    def test_resolved(self, value: Union[int, str]) -> None:
+    def test_resolved(self, value: int | str) -> None:
         """Test resolved."""
         obj = VariableValueLiteral(value)  # type: ignore
         assert obj.resolved
 
     @pytest.mark.parametrize("value", [False, True, 13, "test"])
-    def test_value(self, value: Union[int, str]) -> None:
+    def test_value(self, value: int | str) -> None:
         """Test value."""
         obj = VariableValueLiteral(value)  # type: ignore
         assert obj.value == value
diff --git a/tests/unit/utils/pydantic_validators/__init__.py b/tests/unit/utils/pydantic_validators/__init__.py
new file mode 100644
index 000000000..19f40a399
--- /dev/null
+++ b/tests/unit/utils/pydantic_validators/__init__.py
@@ -0,0 +1,1 @@
+"""Test runway.utils.pydantic_validators."""
diff --git a/tests/unit/utils/pydantic_validators/test__lax_str.py b/tests/unit/utils/pydantic_validators/test__lax_str.py
new file mode 100644
index 000000000..635d25926
--- /dev/null
+++ b/tests/unit/utils/pydantic_validators/test__lax_str.py
@@ -0,0 +1,51 @@
+"""Test runway.utils.pydantic_validators._lax_str."""
+
+from __future__ import annotations
+
+from decimal import Decimal
+from enum import Enum
+from typing import Annotated
+
+import pytest
+from pydantic import BaseModel, ValidationError
+
+from runway.utils.pydantic_validators._lax_str import LaxStr
+
+
+class SomeEnum(str, Enum):
+    """Enum for testing."""
+
+    FOO = "foo"
+
+
+class Model(BaseModel):
+    """Model used for testing."""
+
+    test: Annotated[str | None, LaxStr]
+
+
+@pytest.mark.parametrize(
+    "provided, expected",
+    [
+        ("foo", "foo"),
+        (5, "5"),
+        (1.0, "1.0"),
+        (Decimal(1.0), "1"),
+        (SomeEnum.FOO, "foo"),
+        (b"foo", "foo"),
+        (None, None),
+    ],
+)
+def test__handler(provided: object, expected: str) -> None:
+    """Test _handler."""
+    assert Model.model_validate({"test": provided}).test == expected
+
+
+@pytest.mark.parametrize(
+    "provided",
+    [{"foo": "bar"}, {"foo", "bar"}, ["foo", "bar"], ("foo", "bar")],
+)
+def test_raise_validation_error(provided: object) -> None:
+    """Test _handler unconverted."""
+    with pytest.raises(ValidationError):
+        Model.model_validate({"test": provided})
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test___init__.py
similarity index 93%
rename from tests/unit/utils/test_utils.py
rename to tests/unit/utils/test___init__.py
index 8ff083275..90563a9e1 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test___init__.py
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-import datetime
 import hashlib
 import json
 import logging
@@ -11,14 +10,12 @@
 import sys
 from contextlib import suppress
 from copy import deepcopy
-from decimal import Decimal
-from typing import TYPE_CHECKING, Any, Optional, Union
+from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, patch
 
 import pytest
 
 from runway.utils import (
-    JsonEncoder,
     MutableMap,
     SafeHaven,
     argv,
@@ -34,6 +31,7 @@
     from pathlib import Path
 
     from pytest_mock import MockerFixture
+    from typing_extensions import TypeAlias
 
 MODULE = "runway.utils"
 VALUE = {
@@ -45,23 +43,6 @@
 }
 
 
-class TestJsonEncoder:
-    """Test runway.utils.JsonEncoder."""
-
-    @pytest.mark.parametrize(
-        "provided, expected", [(datetime.datetime.now(), str), (Decimal("1.1"), float)]
-    )
-    def test_supported_types(self, provided: Any, expected: type) -> None:
-        """Test encoding of supported data types."""
-        assert isinstance(JsonEncoder().default(provided), expected)
-
-    @pytest.mark.parametrize("provided", [(None)])
-    def test_unsupported_types(self, provided: Any) -> None:
-        """Test encoding of unsupported data types."""
-        with pytest.raises(TypeError):
-            assert not JsonEncoder().default(provided)
-
-
 class TestMutableMap:
     """Test for the custom MutableMap data type."""
 
@@ -126,7 +107,7 @@ def test_find_default(self) -> None:
         ), "default should be ignored"
 
 
-TestParamsTypeDef = Optional[Union[dict[str, str], list[str], str]]
+TestParamsTypeDef: TypeAlias = "dict[str, str] | list[str] | str | None"
 
 
 class TestSafeHaven:
diff --git a/tests/unit/utils/test__file_hash.py b/tests/unit/utils/test__file_hash.py
index a4dcbc154..e8bc2f4b7 100644
--- a/tests/unit/utils/test__file_hash.py
+++ b/tests/unit/utils/test__file_hash.py
@@ -1,6 +1,5 @@
 """Test runway.utils._file_hash."""
 
-# pyright: basic
 from __future__ import annotations
 
 import hashlib