Skip to main content

Scrython

Scrython is a wrapper for the Scryfall API, designed for an easier use.

Here is a link to the Scryfall API documentation.

Installation

Scrython is available in PyPI, and requires no other dependencies.

pip install scrython

Type Hints

Scrython is a PEP 561 compliant typed package. It ships a py.typed marker, so mypy, pyright, and Pylance all pick up the bundled inline type hints without any extra configuration or stub packages.

import scrython

card = scrython.cards.Named(fuzzy="Lightning Bolt")
reveal_type(card.name)  # should reveal type `str` (mypy may display this as `builtins.str`)

Run your type checker of choice against scrython imports and it will work out of the box:

mypy my_script.py      # or
pyright my_script.py

⚠️ Important: Rate Limiting

Good news! Scrython 2.0 includes built-in rate limiting enabled by default. You no longer need to manually add delays between requests.

Scrython automatically enforces Scryfall's tiered rate limits:

  • 10 requests/second for most endpoints (cards by ID, sets, bulk data, autocomplete, etc.)
  • 2 requests/second for heavier endpoints: Search, Named, Random, and Collection

See Scryfall's rate limit documentation for details.

Automatic Rate Limiting (Default):

import scrython

# Rate limiting is automatic! No delays needed
cards_to_fetch = ['Lightning Bolt', 'Counterspell', 'Black Lotus']

for card_name in cards_to_fetch:
    card = scrython.cards.Named(fuzzy=card_name)  # Automatically rate limited (2/s)
    print(f"{card.name} - {card.set}")

Custom Rate Limits:

# Override the rate for a specific call (5 requests/second)
# Note: the override is scoped to this handler instance only.
# Separate instantiations each get their own limiter.
card = scrython.cards.Named(fuzzy='Lightning Bolt', rate_limit_per_second=5)

# Disable rate limiting entirely (use with caution!)
card = scrython.cards.Named(fuzzy='Lightning Bolt', rate_limit=False)

Legacy Code (Manual Rate Limiting):

If you prefer manual rate limiting or need finer control:

import scrython
import time

# Disable automatic rate limiting and use manual delays
for card_name in cards_to_fetch:
    card = scrython.cards.Named(fuzzy=card_name, rate_limit=False)
    print(f"{card.name} - {card.set}")
    time.sleep(0.5)  # 500ms delay for Named endpoint

Better: Use Bulk Data for Large Datasets

For large-scale data processing, use Scryfall's bulk data downloads instead:

import scrython

# Download all unique cards at once
bulk = scrython.bulk_data.ByType(type='oracle_cards')
cards = bulk.download()

# Process all cards locally without rate limits!
for card in cards:
    print(f"{card['name']} - {card['set']}")

# Bulk data files are updated every 12 hours

Built-in Caching

Scrython 2.0 includes built-in caching with TTL (time-to-live) support:

import scrython

# Enable caching with 1-hour TTL (default)
card = scrython.cards.Named(fuzzy='Lightning Bolt', cache=True)

# First call makes API request
card1 = scrython.cards.Named(fuzzy='Lightning Bolt', cache=True)

# Second call returns cached result (no API request!)
card2 = scrython.cards.Named(fuzzy='Lightning Bolt', cache=True)

# Custom TTL (in seconds)
card = scrython.cards.Named(fuzzy='Lightning Bolt', cache=True, cache_ttl=7200)  # 2 hours

Note: Card prices become unreliable after 24 hours. Consider shorter TTLs for price-sensitive applications.

Legacy Caching (functools.lru_cache):

You can still use Python's built-in caching if preferred:

from functools import lru_cache
import scrython

@lru_cache(maxsize=1000)
def get_card_by_name(name: str):
    """Cache card lookups to avoid duplicate requests."""
    return scrython.cards.Named(fuzzy=name, rate_limit=False)

card1 = get_card_by_name('Lightning Bolt')
card2 = get_card_by_name('Lightning Bolt')  # Cached

Custom User-Agent (Recommended)

Scryfall requests that applications identify themselves with a custom User-Agent:

from scrython.base import ScrythonRequestHandler
import scrython

# Set custom User-Agent for your application
ScrythonRequestHandler.set_user_agent('MyMTGApp/1.0 (contact@example.com)')

# All subsequent requests will use your custom User-Agent
card = scrython.cards.Named(fuzzy='Black Lotus')

Complete Usage Examples

Basic Card Lookup

import scrython

# Fuzzy name search (handles typos)
card = scrython.cards.Named(fuzzy='Light Bolt')
print(card.name)  # "Lightning Bolt"
print(card.mana_cost)  # "{R}"
print(card.type_line)  # "Instant"
print(card.oracle_text)

# Exact name match
card = scrython.cards.Named(exact='Black Lotus')
print(card.prices)
# {'usd': '25000.00', 'usd_foil': None, ...}

Advanced Search

# Search with Scryfall syntax
results = scrython.cards.Search(q='type:creature cmc:1 color:red')

print(f"Found {results.total_cards} cards")

for card in results.data:
    print(f"{card.name} - {card.set_name}")

# Handle pagination
if results.has_more:
    print("More results available - implement pagination as needed")

Getting Specific Cards

# By set code and collector number
card = scrython.cards.ByCodeNumber(code='znr', number='123')

# By various IDs
card = scrython.cards.ByMultiverseId(id=456789)
card = scrython.cards.ByMTGOId(id=67890)
card = scrython.cards.ById(id='5f8287b1-5bb6-4e8f-9d78-8f3e3b3e1c6d')

# Get random card
card = scrython.cards.Random()
card = scrython.cards.Random(q='rarity:mythic')  # Random mythic

Working with Sets

# Get all sets
all_sets = scrython.sets.All()

for set_obj in all_sets.data:
    print(f"{set_obj.name} ({set_obj.code}) - {set_obj.card_count} cards")

# Get specific set
set_obj = scrython.sets.ByCode(code='znr')
print(f"{set_obj.name} released on {set_obj.released_at}")
print(f"Set type: {set_obj.set_type}")

Bulk Data Download

Bulk data files contain all Magic cards and are updated every 12 hours. This is the recommended approach for processing large datasets, as it avoids rate limits entirely.

import scrython

# Get all bulk data options
all_bulk = scrython.bulk_data.All()

for bulk in all_bulk.data:
    print(f"{bulk.name}: {bulk.description}")
    print(f"Compressed size: {bulk.compressed_size / 1_000_000:.1f} MB")

# Download oracle cards (all unique cards with Oracle text)
oracle_cards = scrython.bulk_data.ByType(type='oracle_cards')

# Option 1: Download and return data in memory
cards = oracle_cards.download()
print(f"Downloaded {len(cards)} cards")

# Process without rate limits!
for card in cards:
    if 'Lightning' in card['name']:
        print(card['name'])

# Option 2: Save to file
oracle_cards.download(filepath='oracle_cards.json')
print("Bulk data saved to oracle_cards.json")

# Option 3: Save without returning data (memory efficient)
oracle_cards.download(filepath='oracle_cards.json', return_data=False)

# Option 4: Show progress bar (requires: pip install scrython[progress])
cards = oracle_cards.download(progress=True)

# Available bulk data types:
# - 'oracle_cards': All unique cards with Oracle text
# - 'unique_artwork': All cards with unique artwork
# - 'default_cards': One version of each card
# - 'all_cards': All card printings
# - 'rulings': All card rulings
# - 'art_tags': Community art tags from Scryfall Tagger
# - 'oracle_tags': Community oracle tags from Scryfall Tagger

Note: The download() method automatically detects whether responses are gzip-compressed by checking HTTP Content-Encoding headers. This means it works seamlessly regardless of Scryfall's CDN configuration - you don't need to worry about compression formats.

Tags

The art_tags and oracle_tags bulk files come from Scryfall Tagger. Tags have no dedicated API endpoint, so download the bulk file through bulk_data.ByType(...).download() and wrap each entry in scrython.tags.Object for typed property access. The nested taggings array resolves into typed Tagging objects.

import scrython

bulk = scrython.bulk_data.ByType(type='oracle_tags')
tags = [scrython.tags.Object(tag) for tag in bulk.download()]

tags[0].label                  # e.g. "removal"
tags[0].type                   # "oracle"
tags[0].id                     # stable UUID

# Oracle taggings carry an oracle_id; art taggings carry an illustration_id
for tagging in tags[0].taggings:
    print(tagging.oracle_id, tagging.weight)

Error Handling

from scrython.base import ScryfallError

try:
    card = scrython.cards.Named(exact='Nonexistent Card Name')
except ScryfallError as e:
    print(f"Error {e.status}: {e.details}")
    if e.warnings:
        print(f"Suggestions: {e.warnings}")

Autocomplete

# Get card name suggestions
suggestions = scrython.cards.Autocomplete(q='light')

for name in suggestions.data:
    print(name)
# Output: "Light", "Lightning Bolt", "Lightning Strike", ...

Collection Queries

# Fetch multiple cards by their identifiers
identifiers = [
    {'id': '5f8287b1-5bb6-4e8f-9d78-8f3e3b3e1c6d'},
    {'name': 'Lightning Bolt', 'set': 'lea'},
    {'multiverse_id': 409574}
]
cards = scrython.cards.Collection(data={'identifiers': identifiers})

for card in cards.data:
    print(f"{card.name} - {card.set}")

Accessing Card Properties

card = scrython.cards.Named(fuzzy='Lightning Bolt')

# Core identifiers
print(card.card_id)              # Scryfall UUID
print(card.oracle_id)       # Oracle ID (consistent across reprints)
print(card.multiverse_ids)  # Gatherer IDs

# Gameplay properties
print(card.mana_cost)       # "{R}"
print(card.cmc)             # 1.0
print(card.type_line)       # "Instant"
print(card.oracle_text)     # Card rules text
print(card.colors)          # ["R"]
print(card.legalities)      # Format legality

# Print properties
print(card.artist)          # Artist name
print(card.set_name)        # Full set name
print(card.rarity)          # "common", "uncommon", etc.
print(card.image_uris)      # Image URLs
print(card.prices)          # Price information

# Multi-face cards
if card.card_faces:
    for face in card.card_faces:
        print(f"{face.name}: {face.mana_cost}")

Advanced Features (New in 2.0)

Magic Methods

Cards and other objects now support Python magic methods for better developer experience:

import scrython

card = scrython.cards.Named(fuzzy='Lightning Bolt')

# Readable representation
print(repr(card))  # Object(id='abc123...', name='Lightning Bolt')
print(str(card))   # Lightning Bolt (LEA)

# Equality comparison (by ID)
card1 = scrython.cards.Named(fuzzy='Lightning Bolt')
card2 = scrython.cards.Named(exact='Lightning Bolt')
print(card1 == card2)  # True (same card ID)

# Use in sets and dicts (hashable)
unique_cards = {card1, card2, card3}  # Deduplicates by ID
card_lookup = {card1: 'owned', card2: 'wanted'}

Serialization

Export and import card data easily:

import scrython

card = scrython.cards.Named(fuzzy='Lightning Bolt')

# Export to dict
card_dict = card.to_dict()

# Export to JSON
json_str = card.to_json(indent=2)

# Save to file
with open('card.json', 'w') as f:
    f.write(card.to_json())

# Import from dict (no API call!)
from scrython.cards.cards import Object
restored_card = Object.from_dict(card_dict)

# Export search results
results = scrython.cards.Search(q='bolt')
all_cards = results.to_list()  # List of dicts

Iteration Support

Iterate directly over search results with Pythonic syntax:

import scrython

results = scrython.cards.Search(q='c:red type:instant')

# Direct iteration (current page)
for card in results:
    print(card.name)

# Get length
print(len(results))  # Number of cards in current page

# Auto-pagination through ALL results
for card in results.iter_all():
    print(card.name)  # Automatically fetches all pages

# Works with list comprehensions
names = [card.name for card in results]

# Works with filter
red_cards = [c for c in results if c.has_color('R')]

Convenience Methods

Quick access to common card operations:

import scrython

card = scrython.cards.Named(fuzzy='Lightning Bolt')

# Legality checks
if card.is_legal_in('commander'):
    print('Commander legal!')

# Color checks
if card.has_color('R'):
    print('Red card!')

# Type checks
if card.is_instant:
    print('Instant speed!')

# Also available: is_creature, is_sorcery, is_enchantment,
#                 is_artifact, is_planeswalker

# Price helpers
cheapest = card.lowest_price()
most_expensive = card.highest_price()
print(f'Price range: ${cheapest:.2f} - ${most_expensive:.2f}')

# Image helpers (handles double-faced cards)
url = card.get_image_url(size='large')
if url:
    print(f'Image: {url}')

List Convenience Methods

Transform and filter search results easily:

import scrython

results = scrython.cards.Search(q='bolt')

# Convert to dict keyed by name
by_name = results.as_dict(key='name')
print(by_name['Lightning Bolt'].set)

# Filter results
cheap_cards = results.filter(lambda c: c.lowest_price() and c.lowest_price() < 1.0)

# Map/transform results
card_names = results.map(lambda c: c.name)

# Chaining
lea_names = [c.name for c in results.filter(lambda c: c.set == 'lea')]

Combining Features

Put it all together for powerful workflows:

import scrython

# Search with caching and rate limiting
results = scrython.cards.Search(
    q='c:red cmc<=3',
    cache=True,
    cache_ttl=3600,
    rate_limit_per_second=5
)

# Iterate and filter
affordable_red = []
for card in results.iter_all():
    if card.is_legal_in('commander') and card.has_color('R'):
        price = card.lowest_price()
        if price and price < 5.0:
            affordable_red.append({
                'name': card.name,
                'price': price,
                'type': card.type_line
            })

# Export results
import json
with open('affordable_red.json', 'w') as f:
    json.dump(affordable_red, f, indent=2)

print(f'Found {len(affordable_red)} affordable red cards!')

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scrython-3.0.0.tar.gz (73.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scrython-3.0.0-py3-none-any.whl (51.7 kB view details)

Uploaded Python 3

File details

Details for the file scrython-3.0.0.tar.gz.

File metadata

  • Download URL: scrython-3.0.0.tar.gz
  • Upload date:
  • Size: 73.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for scrython-3.0.0.tar.gz
Algorithm Hash digest
SHA256 14166e896d425b351fa9f7f6721a64d6d44ea6ed5c2a9b88111d0f8d969eb023
MD5 facbc78b33f03d85c80367fc8f2dedd5
BLAKE2b-256 b012de566c4b37dd052c3c1515f6af18091c97da3f4418a9ca4ec201b11d91de

See more details on using hashes here.

File details

Details for the file scrython-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: scrython-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 51.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for scrython-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 abfb8a070b2ed466eb37d2314ee10ee8e7ba4744da17fa29cab7180c9c8ad1ee
MD5 15f9c94e2d0489db3daeb893fedc692d
BLAKE2b-256 083989f68983b4edc848f2f518d0ab264bcafcd7fda8f4b90535354ac95ccae0

See more details on using hashes here.

Release history Release notifications | RSS feed

3.1.0

2 files

This release

3.0.0 This release

2 files

2.2.0

2 files

2.1.0

2 files

2.0.3

2 files

2.0.2

2 files

2.0.1

2 files

2.0.0

2 files

1.11.0

2 files

1.10.1

2 files

1.9.0

2 files

1.8.1

2 files

1.8.0

2 files

1.7.1

1 file

1.7.0

2 files

1.6.2

1 file

1.6.1

1 file

1.6.0

1 file

1.5.0

1 file

1.4.2

1 file

1.4.1

1 file

1.4.0

1 file

1.3.4

1 file

1.3.3

1 file

1.3.2

1 file

1.3.1

1 file

1.3.0

1 file

1.2.0

1 file

1.1.0

1 file

1.0.0

1 file

0.5.0

1 file

0.4.0

1 file

0.3.0

1 file

0.2.0

1 file

0.1.0

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page