Migration
Upgrading Python from 1.x to 2.x
slackblocks 2.0 is a major version bump that focuses on cleaning up internal
correctness issues, modernising the type-annotation surface, and adding several
ergonomic helpers. The good news: for almost all users, upgrading is a
no-op beyond changing the Python version pin.
This page covers what actually changes for you and what's worth adopting once you've upgraded.
Quick checklist
| Step | Action |
|---|---|
| 1 | Confirm you are on Python 3.10 or newer. If not, stay on slackblocks ~= 1.2. |
| 2 | Bump your dependency pin to slackblocks ~= 2.0 (or >=2,<3). |
| 3 | Run your test suite. Existing code should continue to work unchanged. |
| 4 | Optionally adopt the new conveniences below. |
Truly breaking changes
There is exactly one breaking change you might trip over:
Python 3.8 and 3.9 are no longer supported
Both reached upstream end of life (3.8 EOL Oct 2024, 3.9 EOL Oct 2025) and are not receiving security patches.
If you are still on either:
- Stay on the
1.xline until you can upgrade Python. The 1.x line is maintained on a best-effort basis for bugfixes; see the Compatibility page. - Pin via your package manager:
- pip (`requirements.txt`)
- poetry (`pyproject.toml`)
- uv (`pyproject.toml`)
slackblocks>=1,<2
slackblocks = "^1"
"slackblocks>=1,<2"
That's it for breaking changes. Nothing else in the public API was removed or renamed.
What's not breaking, but worth adopting
PlainText and Markdown instead of Text(type_=...)
Before:
from slackblocks import Text, TextType
heading = Text("Welcome", type_=TextType.PLAINTEXT, emoji=True)
body = Text("**bold**", type_=TextType.MARKDOWN, verbatim=True)
After:
from slackblocks import PlainText, Markdown
heading = PlainText("Welcome", emoji=True)
body = Markdown("**bold**", verbatim=True)
PlainText and Markdown are subclasses of Text, so anywhere a Text or
TextLike was accepted before still accepts the new types. The rendered
JSON is byte-identical to the equivalent Text calls.
Workflow.from_url(...) instead of nested Workflow(trigger=Trigger(...))
Before:
from slackblocks import Workflow, Trigger, InputParameter
workflow = Workflow(
trigger=Trigger(
url="https://slack.com/shortcuts/...",
customizable_input_parameters=[
InputParameter(name="env", value="prod"),
InputParameter(name="retries", value="3"),
],
),
)
After:
from slackblocks import Workflow
workflow = Workflow.from_url(
"https://slack.com/shortcuts/...",
env="prod",
retries="3",
)
The verbose form continues to work, so you do not have to migrate.
block_kit_builder_url(...) for browser preview
New helper that turns any block, list of blocks, message, view, or raw dict into a Block Kit Builder URL. Open the URL in your browser to see exactly what Slack will render before you ship.
from slackblocks import SectionBlock, block_kit_builder_url
print(block_kit_builder_url(SectionBlock("Hi")))
# https://app.slack.com/block-kit-builder/#%7B%22blocks%22%3A%5B...%5D%7D
Pass team_id="T0123ABCD" for a workspace-specific URL.
Typed exceptions for finer-grained error handling
InvalidUsageError now has five subclasses, raised in well-defined
situations:
| Subclass | Raised when |
|---|---|
LengthError | A string or list violates a min/max-length constraint. |
RangeError | A numeric value violates a min/max-value constraint. |
TypeMismatchError | Wrong type or unexpected discrete value. |
MutualExclusivityError | Two args that must not both be set were both set. |
MissingRequiredError | At least one of a set of args was required. |
from slackblocks import Button, LengthError
try:
Button(text="x" * 100, action_id="b")
except LengthError as e:
# specifically a length violation
handle_length(e)
Existing except InvalidUsageError blocks continue to catch every
subclass — no change required to upgrade. The new subclasses are purely
additive.
Round-tripping incoming Slack JSON with from_dict
If your app receives Slack interactivity payloads or events and wants to
inspect the blocks, you can now parse them back into slackblocks objects:
import json
from slackblocks import Block
incoming = json.loads(slack_request_body)
block = Block.from_dict(incoming["blocks"][0])
print(block.text.text) # for a SectionBlock, etc.
Block.from_dict reads data["type"] and dispatches to the right subclass.
Composition objects (Text, Option, Confirm, etc.) and the simple
blocks (DividerBlock, FileBlock, HeaderBlock, MarkdownBlock,
ImageBlock, SectionBlock, ContextBlock, VideoBlock) round-trip fully.
Blocks that contain interactive elements (ActionsBlock, InputBlock,
SectionBlock with an accessory, ContextBlock with image elements)
raise NotImplementedError for now; these depend on Element.from_dict
which is planned for a follow-up release.
Two new block types
Slack added these in 2024; they're now available:
from slackblocks import MarkdownBlock, VideoBlock
# Slack's GitHub-flavored markdown block (richer than SectionBlock mrkdwn).
MarkdownBlock(text="# Heading\n\n- item 1\n- item 2")
# Embed a video from a Slack-supported provider.
VideoBlock(
alt_text="Demo",
thumbnail_url="https://example.com/thumb.png",
title="Getting Started",
video_url="https://example.com/video.mp4",
)
Type-checking improvements
The 2.x series ships with substantially tighter type signatures. If you type-check your code with mypy, pyright, or Pyre, expect that:
Text.to_text("hi")is now correctly typed asText(was previouslyText | None). Code that didassert text is not Noneafterto_textcalls can drop the assertion.Button(style="warning")is now flagged at type-check time —styleisLiteral["primary", "danger"] | ButtonStyle | None.ColumnSettings(align="LEFT")is similarly flagged at type-check time.ConversationFilter(include=["bogus"])is flagged.
These constraints already existed at runtime; the difference is that mypy now agrees with the runtime behaviour.
Internal changes you might notice
The _resolve() method on every block, element, and composition object was
refactored to use a shared slackblocks._core.resolve() walker. This is
strictly internal — JSON output is byte-identical to 1.x — but if you have
been subclassing internal classes you may notice the _resolve bodies are
much shorter.
If you are extending slackblocks with custom classes:
- If your custom class has a
_resolvemethod that follows the existing pattern (manualif self.x is not None: out["x"] = self.x._resolve()), it continues to work in 2.x without modification. - If you want, you can switch to using the new
slackblocks._core.resolve/omit_nonehelpers; doing so guarantees recursion into nested_resolveoutputs and is much more concise.
Reporting issues
If you hit a real upgrade problem, please open an issue on
GitHub — include
your Python version, your slackblocks version (old and new), and a
minimal repro.
See Installation and the TypeScript API reference for supported versions and native conventions. This page's historical release-line discussion applies to Python.
See Installation and the Go API reference for supported versions and native conventions. This page's historical release-line discussion applies to Python.
See Installation and the Java API reference for supported versions and native conventions. This page's historical release-line discussion applies to Python.
See Installation and the C# API reference for supported versions and native conventions. This page's historical release-line discussion applies to Python.
See Installation and the Ruby API reference for supported versions and native conventions. This page's historical release-line discussion applies to Python.
Moving to the Rust API
Rust support starts in 2.6.0; there is no previous Rust package API to migrate. When porting code from another implementation:
- Use consuming snake_case builders and handle
build()with?ormatch. - Use
SelectOption,SelectOptionGroupandConfirmationDialogue; names avoid colliding with Rust'sOptionand describe their Slack roles. - Getters borrow values. Use
clone().into_builder()to edit a copy, or move the value into its builder when the old value is no longer needed. - Use concrete values and
Fromconversions into role enums; strings coerce only in text fields. Typed markdown is rejected in plain-only fields at compile time. - Serialize with Serde. HTTP clients and async runtimes stay in the application.
- Block IDs are omitted unless supplied.
MessageResponsefollows shared model defaults, and parsing preserves absent optional fields. Useclear_*()to remove defaults explicitly. - Python's deprecated attachment
Fieldand historical aliases are not copied; the Rust API implements the shared model and documented helper inventory.
See Quick Start, Compatibility, and the Rust API reference for complete signatures and error behavior.
Adopting the PHP API
There is no earlier Slackblocks PHP release to migrate from. Existing raw arrays
can enter through MessagePayload::fromArray() or fromJson(); choose fromJson()
when preserving empty JSON objects matters. Unknown fields become immutable
JsonObject extensions. Modeled names and type cannot be overridden.
From slack-php/slack-php-block-kit, replace mutable/fluent construction with
named constructors and with(). These are separate packages with different APIs;
objects are not interchangeable. Use JSON at the boundary. From Laravel's Slack
notification builder, keep delivery and routing in Laravel and bridge validated
blocks through usingBlockKitTemplate() as shown in Sending Messages.
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Slackblocks as S;
$original = S\SectionBlock::fromJson('{"type":"section","text":{"type":"mrkdwn","text":"Original"}}');
$edited = $original->with(text: 'Updated');
assert($original->text->getText() === 'Original');
echo $edited->toJson();
Construct typed children directly for new code. PlainText and MarkdownText
are distinct classes; text parameters also accept strings with the field's
native plain-text or mrkdwn default. Enums use cases such as
ButtonStyle::Primary, not unchecked strings. Optional constructor null omits
a field; explicit null inside a JsonObject is preserved.
Starting with Elixir
2.8.0 is the first planned Elixir release, so there is no older Elixir API to migrate.
Use native constructors, lists and atom enums rather than translating another
language's builder chains. Names use Slack's wire snake_case.
alias Slackblocks.{Blocks.Section, Objects.MarkdownText}
original = Section.new!(text: "Before")
{:ok, updated} = Section.change(original, text: "After", block_id: "status")
%MarkdownText{text: "Before"} = original.text
%MarkdownText{text: "After"} = updated.text
Slackblocks.to_map!(updated)
Constructor binaries are converted to the correct text object. Direct struct
edits require the actual typed value and are checked on output. change/2
retains unspecified fields and validates an edit atomically. Explicit nil
clears optional fields; it cannot clear a required field.
Parsing preserves absent optional fields as nil, while construction applies
declared defaults. Message defaults to text: "" and mrkdwn: true;
block IDs are never generated. Use checked decode/2 for known model/role JSON.
Arbitrary Slack event payloads belong to your application's event decoder.
When adding slackblocks to an existing application, rebuild Jason's optional Decimal support as described in Installation. Keep application locks and recompile dependencies/protocols before producing an OTP release.