Skip to main content

A wrapper for using the Scryfall API.

Project description

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"Size: {bulk.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!')

Project details


Download files

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

Source Distribution

scrython-2.2.0.tar.gz (73.8 kB view details)

Uploaded Source

Built Distribution

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

scrython-2.2.0-py3-none-any.whl (52.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: scrython-2.2.0.tar.gz
  • Upload date:
  • Size: 73.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for scrython-2.2.0.tar.gz
Algorithm Hash digest
SHA256 5c6e4ffac1b646435bfc80cfc40ea565334df7a544e8eb4f26d879a675813ccd
MD5 b8407a31c6305366f9b93fc34417db77
BLAKE2b-256 40c29d1cee83b445541d9fd2db46e32d64cd1593cbfc64ff12880ab7b2e8d80b

See more details on using hashes here.

File details

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

File metadata

  • Download URL: scrython-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 52.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for scrython-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9d390088e3d5229b3bd1d44cfe9bb14ab280279f2c0db0be92b6da95d579d15c
MD5 73bba2070995ede8a5328b6f3d841077
BLAKE2b-256 317cdd2ef766fc8c2044b562bc451a93481decf52a7f82c1c32b78af39bfe6dc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page