Scrython is a wrapper for the Scryfall API, designed for an easier use.
Here is a link to the Scryfall API documentation.
Scrython is available in PyPI, and requires no other dependencies.
pip install scrythonGood news! Scrython 2.0 now includes built-in rate limiting enabled by default, enforcing Scryfall's 10 requests/second guideline automatically. You no longer need to manually add delays between requests.
Scryfall requires 50-100 milliseconds delay between requests (~10 requests/second maximum).
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
print(f"{card.name} - {card.set}")# Use a slower rate (5 requests/second)
card = scrython.cards.Named(fuzzy='Lightning Bolt', rate_limit_per_second=5)
# Disable rate limiting (use with caution!)
card = scrython.cards.Named(fuzzy='Lightning Bolt', rate_limit=False)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.1) # 100ms delayFor 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 hoursScrython 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 hoursNote: Card prices become unreliable after 24 hours. Consider shorter TTLs for price-sensitive applications.
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') # CachedScryfall 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 ([email protected])')
# All subsequent requests will use your custom User-Agent
card = scrython.cards.Named(fuzzy='Black Lotus')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, ...}# 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")# 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# 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 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 rulingsNote: 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.
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}")# Get card name suggestions
suggestions = scrython.cards.Autocomplete(q='light')
for name in suggestions.data:
print(name)
# Output: "Light", "Lightning Bolt", "Lightning Strike", ...# 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}")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}")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'}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 dictsIterate 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')]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}')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')]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!')