diff --git a/docs/platforms/python/migration/2.x-to-3.x.mdx b/docs/platforms/python/migration/2.x-to-3.x.mdx
new file mode 100644
index 0000000000000..cd0ac66ae9b8e
--- /dev/null
+++ b/docs/platforms/python/migration/2.x-to-3.x.mdx
@@ -0,0 +1,340 @@
+---
+title: Migrate from 2.x to 3.x
+sidebar_order: 8997
+description: "Learn about migrating from sentry-python 2.x to 3.x"
+---
+
+<Alert title="Python SDK 3.0 pre-release">
+
+Version 3.0 of the Sentry Python SDK is currently in pre-release. If you feel like giving it a spin, check out [our most recent releases](https://pypi.org/project/sentry-sdk/#history). Your feedback at this stage is invaluable, so please let us know about your experience, whether positive or negative, [on GitHub](https://github.com/getsentry/sentry-python/discussions/3936) or [on Discord](https://discord.gg/wdNEHETs87): How did the migration go? Did you encounter any issues? Is everything working as expected?
+
+</Alert>
+
+This guide describes the common patterns involved in migrating to version `3.x` of the `sentry-python` SDK. For the full list of changes, check out the [detailed migration guide in the repository](https://github.com/getsentry/sentry-python/blob/potel-base/MIGRATION_GUIDE.md).
+
+
+## Python Version Support
+
+Sentry Python SDK `3.x` only supports Python 3.7 and higher. If you're on an older Python version, you'll need to stay on an older version of the SDK:
+
+- Python 2.7-3.5: SDK `1.x`
+- Python 3.6: SDK `2.x`
+
+
+## Configuration
+
+The `enable_tracing` option was removed. Use [`traces_sample_rate`](/platforms/python/configuration/options/#traces_sample_rate) directly, or configure a [`traces_sampler`](/platforms/python/configuration/options/#traces_sampler) for more fine-grained control over which spans should be sampled.
+
+```python diff
+  sentry_sdk.init(
+-     enable_tracing=True,
++     traces_sample_rate=1.0,
+  )
+```
+
+The deprecated `propagate_traces` option was removed. Use [`trace_propagation_targets`](/platforms/python/configuration/options/#trace_propagation_targets) instead.
+
+```python diff
+  sentry_sdk.init(
+      # don't propagate trace info downstream
+-     propagate_traces=False,
++     trace_propagation_targets=[],
+  )
+```
+
+Note that this only affects the global SDK option. The [`propagate_traces`](/platforms/python/integrations/celery/#options) option of the Celery integration remains unchanged.
+
+The `profiles_sample_rate` and `profiler_mode` options previously nested under `_experiments` have been removed. They're replaced by top-level options of the same name:
+
+```python diff
+  sentry_sdk.init(
+-     _experiments={
+-         "profiles_sample_rate": 1.0,
+-         "profiler_mode": "thread",
+-     },
++     profiles_sample_rate=1.0,
++     profiler_mode="thread",
+  )
+```
+
+## API Changes
+
+`add_attachment()` is now a part of the top-level level API and should be imported and used directly from `sentry_sdk`.
+
+```python diff
+  import sentry_sdk
+
+- scope = sentry_sdk.get_current_scope()
+- scope.add_attachment(bytes=b"Hello World!", filename="attachment.txt")
++ sentry_sdk.add_attachment(bytes=b"Hello World!", filename="attachment.txt")
+```
+
+Using `sentry_sdk.add_attachment()` directly also makes sure the attachment is added to the correct scope internally.
+
+### Custom Tracing API
+
+Tracing in the Sentry Python SDK `3.x` is powered by [OpenTelemetry](https://opentelemetry.io/) in the background, which also means we're moving away from the Sentry-specific concept of transactions and towards a span-only future. `sentry_sdk.start_transaction()` is now deprecated in favor of `sentry_sdk.start_span()`.
+
+```python diff
+- with sentry_sdk.start_transaction():
++ with sentry_sdk.start_span():
+      ...
+```
+
+Any spans without a parent span will become transactions by default. If you want to avoid promoting a span without a parent to a transaction, you can pass the `only_if_parent=True` keyword argument to `sentry_sdk.start_span()`.
+
+`sentry_sdk.start_transaction()` and `sentry_sdk.start_span()` no longer take the following arguments: `trace_id`, `baggage`, `span_id`, `parent_span_id`. Use `sentry_sdk.continue_trace()` for propagating trace data.
+
+`sentry_sdk.continue_trace()` no longer returns a `Transaction` and is now a context manager. To continue a trace from headers or environment variables, start a new span inside `sentry_sdk.continue_trace()`:
+
+```python diff
+- transaction = sentry_sdk.continue_trace({...})
+- with sentry_sdk.start_transaction(transaction=transaction):
+-     ...
++ with sentry_sdk.continue_trace({...}):
++     with sentry_sdk.start_span():
++         ...
+```
+
+The functions `continue_from_headers`, `continue_from_environ` and `from_traceparent` have been removed. Use the `sentry_sdk.continue_trace()` context manager instead.
+
+
+## Span Data
+
+In OpenTelemetry, there is no concept of separate categories of data on a span: everything is simply a span attribute. This is a concept the Sentry SDK is also adopting. We deprecated `set_data()` and added a new span method called `set_attribute()`:
+
+```python diff
+  with sentry_sdk.start_span(...) as span:
+-      span.set_data("my_attribute", "my_value")
++      span.set_attribute("my_attribute", "my_value")
+```
+
+You can also set attributes directly when creating the span. This has the advantage that these initial attributes will be accessible in the sampling context in your `traces_sampler`/`profiles_sampler` (see also the [Sampling section](#sampling)).
+
+```python
+with sentry_sdk.start_span(attributes={"my_attribute": "my_value"}):
+    ...
+```
+
+<Alert title="Span attribute type restrictions" level="warning">
+
+There are important type restrictions to consider when setting attributes on a span via `span.set_attribute()` and `start_span(attributes={...})`. The keys must be non-empty strings and the values can only be several primitive types (excluding `None`) or a list of a single primitive type. See [the OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/common/#attribute) for details.
+
+Note that since the SDK is now exclusively using span attributes, this restriction applies to other ways of setting data on a span as well like `span.set_data()`, `span.set_measurement()`, `span.set_context()`.
+
+</Alert>
+
+
+## Sampling
+
+It's no longer possible to change the sampling decision of a span by setting `span.sampled` directly after the span has been created. Use either a custom `traces_sampler` (preferred) or the `sampled` argument to `start_span()` for determining whether a span should be sampled.
+
+```python
+with sentry_sdk.start_span(sampled=True) as span:
+    ...
+```
+
+<Alert title="Sampling non-root spans" level="warning">
+
+Both `traces_sampler` and the `sampled` argument will only influence whether root spans (transactions) are sampled. They can't be used for sampling child spans.
+
+</Alert>
+
+The `sampling_context` argument of `traces_sampler` and `profiles_sampler` has changed considerably for spans coming from our auto-instrumented integrations. As a consequence of using OpenTelemetry under the hood, spans can only carry specific, primitive types of data. This prevents us from making custom objects, for example, the `Request` object for several web frameworks, accessible on the span.
+
+<Expandable title='AIOHTTP sampling context changes'>
+  The AIOHTTP integration doesn't add the `aiohttp_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:
+
+    | Request property  | Sampling context key(s)         |
+    | ----------------- | ------------------------------- |
+    | `path`            | `url.path`                      |
+    | `query_string`    | `url.query`                     |
+    | `method`          | `http.request.method`           |
+    | `host`            | `server.address`, `server.port` |
+    | `scheme`          | `url.scheme`                    |
+    | full URL          | `url.full`                      |
+    | `request.headers` | `http.request.header.{header}`  |
+</Expandable>
+
+<Expandable title='Celery sampling context changes'>
+  The Celery integration doesn't add the `celery_job` dictionary anymore. Instead, the individual keys are now available as:
+
+  | Dictionary keys        | Sampling context key        | Example                        |
+  | ---------------------- | --------------------------- | ------------------------------ |
+  | `celery_job["args"]`   | `celery.job.args.{index}`   | `celery.job.args.0`            |
+  | `celery_job["kwargs"]` | `celery.job.kwargs.{kwarg}` | `celery.job.kwargs.kwarg_name` |
+  | `celery_job["task"]`   | `celery.job.task`           |                                |
+</Expandable>
+
+<Expandable title='Tornado sampling context changes'>
+  The Tornado integration doesn't add the `tornado_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:
+
+    | Request property  | Sampling context key(s)                             |
+    | ----------------- | --------------------------------------------------- |
+    | `path`            | `url.path`                                          |
+    | `query`           | `url.query`                                         |
+    | `protocol`        | `url.scheme`                                        |
+    | `method`          | `http.request.method`                               |
+    | `host`            | `server.address`, `server.port`                     |
+    | `version`         | `network.protocol.name`, `network.protocol.version` |
+    | full URL          | `url.full`                                          |
+    | `request.headers` | `http.request.header.{header}`                      |
+</Expandable>
+
+<Expandable title='WSGI sampling context changes'>
+  The WSGI integration doesn't add the `wsgi_environ` object anymore. Instead, the individual properties of the environment are accessible, if available, as follows:
+
+    | Env property      | Sampling context key(s)                           |
+    | ----------------- | ------------------------------------------------- |
+    | `PATH_INFO`       | `url.path`                                        |
+    | `QUERY_STRING`    | `url.query`                                       |
+    | `REQUEST_METHOD`  | `http.request.method`                             |
+    | `SERVER_NAME`     | `server.address`                                  |
+    | `SERVER_PORT`     | `server.port`                                     |
+    | `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` |
+    | `wsgi.url_scheme` | `url.scheme`                                      |
+    | full URL          | `url.full`                                        |
+    | `HTTP_*`          | `http.request.header.{header}`                    |
+</Expandable>
+
+<Expandable title='ASGI sampling context changes'>
+  The ASGI integration doesn't add the `asgi_scope` object anymore. Instead, the individual properties of the scope, if available, are accessible as follows:
+
+    | Scope property | Sampling context key(s)         |
+    | -------------- | ------------------------------- |
+    | `type`         | `network.protocol.name`         |
+    | `scheme`       | `url.scheme`                    |
+    | `path`         | `url.path`                      |
+    | `query`        | `url.query`                     |
+    | `http_version` | `network.protocol.version`      |
+    | `method`       | `http.request.method`           |
+    | `server`       | `server.address`, `server.port` |
+    | `client`       | `client.address`, `client.port` |
+    | full URL       | `url.full`                      |
+    | `headers`      | `http.request.header.{header}`  |
+</Expandable>
+
+<Expandable title='RQ sampling context changes'>
+  The RQ integration doesn't add the `rq_job` object anymore. Instead, the individual properties of the job and the queue, if available, are accessible as follows:
+
+    | RQ property     | Sampling context key         | Example                |
+    | --------------- | ---------------------------- | ---------------------- |
+    | `rq_job.args`   | `rq.job.args.{index}`        | `rq.job.args.0`        |
+    | `rq_job.kwargs` | `rq.job.kwargs.{kwarg}`      | `rq.job.args.my_kwarg` |
+    | `rq_job.func`   | `rq.job.func`                |                        |
+    | `queue.name`    | `messaging.destination.name` |                        |
+    | `rq_job.id`     | `messaging.message.id`       |                        |
+
+    Note that `rq.job.args`, `rq.job.kwargs`, and `rq.job.func` are serialized and not the actual objects on the job.
+</Expandable>
+
+<Expandable title='AWS Lambda sampling context changes'>
+  The AWS Lambda integration doesn't add the `aws_event` and `aws_context` objects anymore. Instead, the following, if available, is accessible:
+
+    | AWS property                                | Sampling context key(s)         |
+    | ------------------------------------------- | ------------------------------- |
+    | `aws_event["httpMethod"]`                   | `http.request.method`           |
+    | `aws_event["queryStringParameters"]`        | `url.query`                     |
+    | `aws_event["path"]`                         | `url.path`                      |
+    | full URL                                    | `url.full`                      |
+    | `aws_event["headers"]["X-Forwarded-Proto"]` | `network.protocol.name`         |
+    | `aws_event["headers"]["Host"]`              | `server.address`                |
+    | `aws_context["function_name"]`              | `faas.name`                     |
+    | `aws_event["headers"]`                      | `http.request.headers.{header}` |
+</Expandable>
+
+<Expandable title='GCP sampling context changes'>
+  The GCP integration doesn't add the `gcp_env` and `gcp_event` keys anymore. Instead, the following, if available, is accessible:
+
+    | Old sampling context key          | New sampling context key       |
+    | --------------------------------- | ------------------------------ |
+    | `gcp_env["function_name"]`        | `faas.name`                    |
+    | `gcp_env["function_region"]`      | `faas.region`                  |
+    | `gcp_env["function_project"]`     | `gcp.function.project`         |
+    | `gcp_env["function_identity"]`    | `gcp.function.identity`        |
+    | `gcp_env["function_entry_point"]` | `gcp.function.entry_point`     |
+    | `gcp_event.method`                | `http.request.method`          |
+    | `gcp_event.query_string`          | `url.query`                    |
+    | `gcp_event.headers`               | `http.request.header.{header}` |
+</Expandable>
+
+The ability to set `custom_sampling_context` on `start_transaction` was removed. If there is custom data that you want to have accessible in the `sampling_context` of a `traces_sampler` or `profiles_sampler`, set it on the span via the `attributes` argument, as all span attributes are now included in the `sampling_context` by default:
+
+```python diff
+- with start_transaction(custom_sampling_context={"custom_attribute": "custom_value"}):
++ with start_span(attributes={"custom_attribute": "custom_value"}) as span:
+      # custom_attribute will now be accessible in the sampling context
+      # of your traces_sampler/profiles_sampler
+      ...
+```
+
+<Alert title="Span attribute type restrictions" level="warning">
+
+As mentioned above, span attribute keys must be non-empty strings and values can only be several primitive types (excluding `None`) or a list of a single primitive type. See [the OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/common/#attribute) for details.
+
+</Alert>
+
+
+## Errors
+
+We've updated how we handle `ExceptionGroup`s. You will now get more data if `ExceptionGroup`s appear in chained exceptions. As an indirect consequence, you might notice a change in how issues are grouped in Sentry.
+
+
+## Integrations
+
+Additional integrations will now be activated automatically if the SDK detects the respective package is installed: Ariadne, ARQ, asyncpg, Chalice, clickhouse-driver, GQL, Graphene, huey, Loguru, PyMongo, Quart, Starlite, Strawberry. You can [opt-out of specific integrations with the `disabled_integrations` option](/platforms/python/integrations/#disabling-integrations).
+
+We no longer support Django older than 2.0, trytond older than 5.0, and Falcon older than 3.0.
+
+### Logging
+
+The logging integration, which implements out-of-the-box support for the Python standard library `logging` framework, doesn't capture error logs as events anymore by default. The original behavior can still be achieved by providing a custom `event_level` to the `LoggingIntegration`:
+
+```python
+sentry_sdk.init(
+    integrations=[
+        # capture error, critical, exception logs
+        # and send them to Sentry as errors
+        LoggingIntegration(event_level="ERROR"),
+    ],
+)
+```
+
+### clickhouse-driver
+
+The query being executed is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).
+
+### PyMongo
+
+The PyMongo integration no longer sets tags automatically. The data is still accessible via span attributes.
+
+The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes.
+
+### Redis
+
+In Redis pipeline spans, there is no `span["data"]["redis.commands"]` that contains a dictionary `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}`. Instead, there is `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`).
+
+
+## Measurements
+
+The `set_measurement()` API was removed. You can set custom attributes on the span instead with `set_attribute()`.
+
+
+## Sessions
+
+The `auto_session_tracking()` context manager was removed. Use `track_session()` instead.
+
+
+## Scope
+
+Setting `Scope.user` directly is no longer supported. Use `Scope.set_user()` instead.
+
+
+## Metrics
+
+The `sentry_sdk.metrics` API doesn't exist anymore in SDK `3.x` as the [metrics beta has come to an end](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End). The associated experimental options `enable_metrics`, `before_emit_metric` and `metric_code_locations` have been removed as well.
+
+
+## Internals
+
+There is no concept of a hub anymore and all APIs and attributes that were connected to hubs have been removed.
