A web development framework for Python that enables building fast, beautiful, and interactive web applications using reactive programming principles.
—
Type definitions, exception classes, and utility types used throughout the Shiny framework. This module provides the foundational types that enable type safety and proper error handling in Shiny applications.
Custom exception types for handling different error conditions in Shiny applications.
class SafeException(Exception):
"""
Exception that is safe to display to users in production.
Unlike regular exceptions, SafeException messages are shown to users
even when error sanitization is enabled, making them suitable for
user-facing error messages.
"""
def __init__(self, message: str) -> None:
"""Create a safe exception with a user-friendly message."""
class SilentException(Exception):
"""
Exception that fails silently without displaying error messages.
Used internally by functions like req() to cancel computation
without showing error messages to users.
"""
def __init__(self, message: str = "") -> None:
"""Create a silent exception."""
class SilentCancelOutputException(SilentException):
"""
Silent exception that cancels output without clearing existing content.
Similar to SilentException but preserves any existing output content
instead of clearing it.
"""
def __init__(self, message: str = "") -> None:
"""Create a silent cancel output exception."""from shiny.types import SafeException, SilentException
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.text
def data_summary():
try:
data = load_data(input.data_source())
return f"Loaded {len(data)} records"
except FileNotFoundError:
# Safe to show to users
raise SafeException("The selected data file could not be found. Please check your selection.")
except PermissionError:
# Safe user message for permission issues
raise SafeException("You don't have permission to access this data file.")
except Exception as e:
# Generic error - don't expose internal details
raise SafeException("An error occurred while loading the data. Please try again.")
@output
@render.plot
def analysis_plot():
# Use req() which raises SilentException internally
req(input.x_variable(), input.y_variable())
data = current_data()
if len(data) == 0:
# Silently cancel without showing error
raise SilentException("No data available")
return create_plot(data, input.x_variable(), input.y_variable())
@output
@render.table
def filtered_data():
base_data = get_base_data()
# Apply filters
filters = input.active_filters()
if not filters:
# Cancel output but don't clear existing table
raise SilentCancelOutputException("No filters active")
return apply_filters(base_data, filters)Type definitions for handling file uploads and file information.
class FileInfo(TypedDict):
"""
Information about an uploaded file.
Contains metadata about files uploaded through file input controls,
providing access to file properties and the server-side file path.
"""
name: str
"""Original filename as provided by the user."""
size: int
"""File size in bytes."""
type: str
"""MIME type of the uploaded file."""
datapath: str
"""Server-side path where the uploaded file is stored."""from shiny.types import FileInfo
import pandas as pd
import os
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.text
def file_info():
file: FileInfo | None = input.uploaded_file()
if file is None:
return "No file uploaded"
# Access file metadata
size_mb = file["size"] / (1024 * 1024)
return f"""
File Information:
- Name: {file['name']}
- Size: {size_mb:.2f} MB
- Type: {file['type']}
- Server Path: {file['datapath']}
"""
@output
@render.data_frame
def uploaded_data():
file: FileInfo | None = input.data_file()
if file is None:
return pd.DataFrame() # Empty DataFrame
# Validate file type
if not file["type"].startswith("text/csv"):
raise SafeException("Please upload a CSV file")
# Validate file size (10MB limit)
if file["size"] > 10 * 1024 * 1024:
raise SafeException("File size must be less than 10MB")
try:
# Read file using server path
data = pd.read_csv(file["datapath"])
return data
except pd.errors.EmptyDataError:
raise SafeException("The uploaded file is empty")
except pd.errors.ParserError:
raise SafeException("Unable to parse the CSV file. Please check the format.")
@reactive.effect
@reactive.event(input.process_file)
def process_uploaded_file():
file: FileInfo | None = input.processing_file()
if file is None:
return
# Create processed filename
base_name = os.path.splitext(file["name"])[0]
processed_name = f"{base_name}_processed.csv"
# Process the file
try:
original_data = pd.read_csv(file["datapath"])
processed_data = perform_data_processing(original_data)
# Save processed file
output_path = f"/tmp/{processed_name}"
processed_data.to_csv(output_path, index=False)
# Trigger download
session.download("processed_download", output_path)
except Exception as e:
raise SafeException(f"Error processing file: {str(e)}")Type definitions for image rendering and display.
class ImgData(TypedDict):
"""
Image data structure for render.image() output.
Provides complete control over image rendering including source,
dimensions, accessibility, and styling options.
"""
src: str
"""Image source URL or data URI."""
width: NotRequired[str | float]
"""Image width (CSS units or pixels)."""
height: NotRequired[str | float]
"""Image height (CSS units or pixels)."""
alt: NotRequired[str]
"""Alt text for accessibility."""
style: NotRequired[str]
"""CSS styles to apply to the image."""
coordmap: NotRequired[Any]
"""Coordinate mapping for interactive plots."""from shiny.types import ImgData
import base64
import io
from PIL import Image, ImageDraw
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.image
def dynamic_chart() -> ImgData:
# Generate image based on inputs
width, height = 400, 300
img = Image.new('RGB', (width, height), color='white')
draw = ImageDraw.Draw(img)
# Draw content based on user inputs
title = input.chart_title() or "Default Chart"
color = input.chart_color() or "blue"
# Draw some sample content
draw.rectangle([50, 50, width-50, height-50], outline=color, width=3)
draw.text((width//2 - 50, 20), title, fill='black')
# Convert to base64 data URI
buffer = io.BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
img_data = base64.b64encode(buffer.getvalue()).decode()
return {
"src": f"data:image/png;base64,{img_data}",
"width": "100%",
"height": "300px",
"alt": f"Dynamic chart: {title}",
"style": "border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"
}
@output
@render.image
def responsive_image() -> ImgData:
# Create responsive image based on device characteristics
pixel_ratio = session.client_data.pixelratio
# Generate high-DPI image if needed
base_width = 300
base_height = 200
actual_width = int(base_width * pixel_ratio)
actual_height = int(base_height * pixel_ratio)
img = create_high_resolution_image(actual_width, actual_height)
# Save to temporary file
temp_path = f"/tmp/responsive_img_{id(session)}.png"
img.save(temp_path, format='PNG', dpi=(96 * pixel_ratio, 96 * pixel_ratio))
return {
"src": temp_path,
"width": f"{base_width}px",
"height": f"{base_height}px",
"alt": "Responsive high-DPI image",
"style": f"max-width: 100%; height: auto;"
}
@output
@render.image
def plot_with_interaction() -> ImgData:
# Generate plot that supports click interactions
plot_data = get_plot_data()
fig = create_interactive_plot(plot_data)
# Save plot with coordinate mapping
plot_path = "/tmp/interactive_plot.png"
fig.savefig(plot_path, dpi=150, bbox_inches='tight')
# Create coordinate mapping for click events
coord_map = generate_coordinate_mapping(fig)
return {
"src": plot_path,
"width": "800px",
"height": "600px",
"alt": "Interactive plot - click for details",
"coordmap": coord_map
}General utility types and constants used throughout the framework.
class MISSING_TYPE:
"""
Type for the MISSING sentinel value.
Used to distinguish between None (which is a valid value) and
a parameter that was not provided at all.
"""
def __repr__(self) -> str:
return "MISSING"
MISSING: MISSING_TYPE
"""
Sentinel value indicating a missing/unspecified parameter.
Used throughout Shiny to distinguish between None (a valid value)
and parameters that were not provided by the user.
"""
Jsonifiable = str | int | float | bool | None | dict[str, Any] | list[Any]
"""
Type alias for values that can be JSON-serialized.
Represents the types that can be safely converted to JSON for
client-server communication in Shiny applications.
"""from shiny.types import MISSING, MISSING_TYPE, Jsonifiable
def optional_parameter_function(
required_param: str,
optional_param: str | MISSING_TYPE = MISSING
) -> str:
"""Function demonstrating MISSING sentinel usage."""
if optional_param is MISSING:
# Parameter was not provided
return f"Required: {required_param}, Optional: not provided"
else:
# Parameter was provided (could be None)
return f"Required: {required_param}, Optional: {optional_param}"
# Usage examples
result1 = optional_parameter_function("hello")
# "Required: hello, Optional: not provided"
result2 = optional_parameter_function("hello", "world")
# "Required: hello, Optional: world"
result3 = optional_parameter_function("hello", None)
# "Required: hello, Optional: None" (None is a valid value)
# JSON serialization helper
def send_data_to_client(data: Jsonifiable) -> None:
"""Send JSON-serializable data to client."""
import json
try:
json_string = json.dumps(data)
session.send_custom_message("data_update", {"data": data})
except TypeError as e:
raise SafeException(f"Data is not JSON-serializable: {e}")
def server(input: Inputs, output: Outputs, session: Session):
@reactive.effect
@reactive.event(input.send_data)
def send_analysis_results():
results = get_analysis_results()
# Ensure data is JSON-serializable
json_data: Jsonifiable = {
"summary": results.summary_dict(),
"metrics": results.metrics_list(),
"timestamp": datetime.now().isoformat(),
"success": True
}
send_data_to_client(json_data)
# Function using MISSING sentinel
def create_plot_with_optional_title(
data: pd.DataFrame,
title: str | MISSING_TYPE = MISSING
):
fig, ax = plt.subplots()
ax.plot(data['x'], data['y'])
if title is not MISSING:
ax.set_title(title)
else:
# Auto-generate title
ax.set_title(f"Plot of {data.columns[1]} vs {data.columns[0]}")
return fig
@output
@render.plot
def main_plot():
data = current_data()
# Title input might be empty string, None, or missing
user_title = input.plot_title()
if user_title: # Non-empty string
return create_plot_with_optional_title(data, user_title)
else: # Empty string or None - use auto title
return create_plot_with_optional_title(data) # MISSING usedType aliases for HTML and UI component construction.
# From htmltools (re-exported by shiny.types)
Tag: TypeAlias
"""HTML tag object."""
TagAttrs: TypeAlias
"""HTML tag attributes dictionary."""
TagAttrValue: TypeAlias
"""Valid values for HTML tag attributes."""
TagChild: TypeAlias
"""Valid child content for HTML tags."""
TagList: TypeAlias
"""List of HTML tags or tag children."""from shiny.types import Tag, TagChild, TagAttrs
from shiny import ui
def create_custom_card(
title: str,
content: TagChild,
**attrs: TagAttrs
) -> Tag:
"""Create a custom card component."""
return ui.div(
ui.div(
ui.h4(title, class_="card-title"),
class_="card-header"
),
ui.div(
content,
class_="card-body"
),
class_="card",
**attrs
)
def create_info_section(
items: list[tuple[str, TagChild]]
) -> Tag:
"""Create an information section from key-value pairs."""
info_items = []
for key, value in items:
info_items.append(
ui.div(
ui.strong(f"{key}: "),
value,
class_="info-item"
)
)
return ui.div(
*info_items,
class_="info-section"
)
# Usage in server function
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.ui
def dynamic_card():
return create_custom_card(
title="Analysis Results",
content=ui.div(
ui.p(f"Dataset has {len(current_data())} rows"),
ui.p(f"Selected variables: {input.variables()}")
),
id="results-card",
style="margin: 20px 0;"
)
@output
@render.ui
def system_info():
return create_info_section([
("Session ID", str(id(session))),
("Client IP", session.client_data.url_hostname),
("User Agent", "Modern Browser"),
("Connected At", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
])Utilities for runtime type checking and validation.
def is_jsonifiable(obj: Any) -> bool:
"""
Check if an object can be JSON-serialized.
Args:
obj: Object to check.
Returns:
True if object is JSON-serializable.
"""
def validate_file_info(obj: Any) -> FileInfo:
"""
Validate and return a FileInfo object.
Args:
obj: Object to validate as FileInfo.
Returns:
Validated FileInfo object.
Raises:
TypeError: If object is not a valid FileInfo.
"""
def validate_img_data(obj: Any) -> ImgData:
"""
Validate and return an ImgData object.
Args:
obj: Object to validate as ImgData.
Returns:
Validated ImgData object.
Raises:
TypeError: If object is not a valid ImgData.
"""from shiny.types import is_jsonifiable, validate_file_info, validate_img_data
def server(input: Inputs, output: Outputs, session: Session):
@reactive.calc
def safe_json_data():
raw_data = get_raw_analysis_results()
# Ensure all data is JSON-serializable before sending to client
cleaned_data = {}
for key, value in raw_data.items():
if is_jsonifiable(value):
cleaned_data[key] = value
else:
# Convert non-serializable data to string representation
cleaned_data[key] = str(value)
return cleaned_data
@output
@render.text
def file_validation_result():
uploaded = input.data_file()
if uploaded is None:
return "No file uploaded"
try:
# Validate file info structure
file_info = validate_file_info(uploaded)
return f"Valid file: {file_info['name']} ({file_info['size']} bytes)"
except TypeError as e:
return f"Invalid file info: {e}"
@output
@render.image
def validated_image():
try:
img_data = generate_image_data()
# Validate image data structure
validated = validate_img_data(img_data)
return validated
except TypeError as e:
# Return error image
return {
"src": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCI+PHRleHQgeD0iMTAiIHk9IjUwIj5FcnJvcjwvdGV4dD48L3N2Zz4=",
"alt": f"Image generation error: {e}"
}Install with Tessl CLI
npx tessl i tessl/pypi-shiny