Python library to search Magic The Gathering cards information on scryfall.com
Project description
PyScryfall: A WIP/unofficial Scryfall API Wrapper
Python client for the Scryfall REST API: search Magic: The Gathering cards by name or Scryfall ID and work with typed card payloads (dataclass models). Fully compliant with Scryfall API guidelines including rate limiting and required headers.
Requirements: Python 3.12+, requests. Network access is required for live API calls.
Installation
Using uv (recommended for this repo):
uv sync
That creates or updates .venv, installs runtime dependencies, and includes the dev dependency group (pytest) by default via default-groups in pyproject.toml.
To install only production dependencies in another workflow, use your tool’s equivalent of installing the pyscryfall project with its [project] dependencies.
Package layout
The library lives under src/pyscryfall/ on disk. After uv sync or pip install, you import it as pyscryfall. Modules split HTTP entry points, typed JSON models, errors, and internal parsing helpers.
Repository layout (high level):
src/pyscryfall/ # installable package (api, schemas, exceptions, helpers, __init__)
tests/
pyproject.toml
flowchart LR
subgraph public [Public surface]
init["pyscryfall/__init__.py"]
end
subgraph impl [Implementation]
api["api.py"]
schemas["schemas.py"]
exceptions["exceptions.py"]
helpers["helpers.py"]
end
init --> api
init --> schemas
init --> exceptions
api --> schemas
api --> exceptions
schemas --> helpers
| Module | Role |
|---|---|
pyscryfall.api |
GET requests to Scryfall, JSON parsing, validation of object field, construction of ScryfallCard / ScryfallCardList. |
pyscryfall.schemas |
Dataclasses mirroring Scryfall card and list JSON; from_dict / to_dict (and list loads / dumps). |
pyscryfall.exceptions |
ScryfallApiError for HTTP failures and API error payloads; ScryfallErrorBody for structured error fields. |
pyscryfall.helpers |
Internal helpers (_optional_model, _list_of, …) used by schemas; not part of the public __all__. |
The package root re-exports the search functions, core card types, and exceptions (see src/pyscryfall/__init__.py __all__).
API and data flow
High-level flow from your code to typed objects:
sequenceDiagram
participant App as YourCode
participant API as pyscryfall.api
participant HTTP as ScryfallHTTPServer
participant Sch as schemas
App->>API: search_cards_by_name or search_card_by_id
API->>API: enforce rate limiting (10 req/sec)
API->>HTTP: GET cards/search or GET cards/id (with User-Agent)
HTTP-->>API: JSON body
API->>API: parse JSON, check object type
alt success list
API->>Sch: ScryfallCardList.from_dict
Sch-->>App: ScryfallCardList
else success card
API->>Sch: ScryfallCard.from_dict
Sch-->>App: ScryfallCard
else HTTP error or object error
API-->>App: ScryfallApiError
end
Core Search Functions
search_cards_by_name(name, …)→GET /cards/searchwith aname:"…"query (quotes escaped). ReturnsScryfallCardList(first page, up to 175 cards).search_card_by_id(card_id, …)→GET /cards/:id. Returns a singleScryfallCard.
Named Search Functions
search_card_by_name_exact(name, …)→GET /cards/named?exact=for exact name matching. Returns a singleScryfallCard.search_card_by_name_fuzzy(name, …)→GET /cards/named?fuzzy=for fuzzy matching (handles typos). Returns a singleScryfallCard.
Pagination
search_cards_by_name_all(name, …)→ Generator that automatically followsnext_pageURLs to yield all matching cards across all pages.
Other Endpoints
get_random_card(q=None, …)→GET /cards/randomwith optional query filter.autocomplete_card_name(q, …)→GET /cards/autocompletefor name suggestions. ReturnsScryfallCatalog.get_card_rulings(card_id, …)→GET /cards/:id/rulings. ReturnsScryfallRulingList.
All functions accept optional session (requests.Session) and timeout. Base URL defaults to https://api.scryfall.com; override with environment variable SCRYFALL_BASE_URL (e.g. for tests or mocks).
Class and composition model
ScryfallCard is the main aggregate: many optional fields and nested dataclasses. ScryfallCardList wraps a page of cards.
classDiagram
direction TB
class ScryfallCardList {
+str object
+int total_cards
+bool has_more
+list data
+str next_page
+from_dict()
+loads()
+to_dict()
+dumps()
}
class ScryfallCard {
+str id
+str name
+from_dict()
+to_dict()
}
class CardFace {
+from_dict()
}
class ImageUris
class Prices
class PreviewInfo
class RelatedUris
class PurchaseUris
class ScryfallRelatedCard
ScryfallCardList "1" --> "*" ScryfallCard : data
ScryfallCard "0..*" --> CardFace : card_faces
ScryfallCard "0..1" --> ImageUris : image_uris
ScryfallCard "0..1" --> Prices : prices
ScryfallCard "0..1" --> PreviewInfo : preview
ScryfallCard "0..1" --> RelatedUris : related_uris
ScryfallCard "0..1" --> PurchaseUris : purchase_uris
ScryfallCard "0..*" --> ScryfallRelatedCard : all_parts
CardFace "0..1" --> ImageUris : image_uris
Errors from the library use ScryfallApiError: message, optional http_status, and optional body (ScryfallErrorBody with code, details, status, etc.) when Scryfall returns an error object.
Usage examples
Search by name (first page)
from pyscryfall import search_cards_by_name, ScryfallCardList
result: ScryfallCardList = search_cards_by_name("Lightning Bolt")
print(result.total_cards, result.has_more)
for card in result.data:
print(card.name, card.set_name, card.collector_number)
Optional arguments match Scryfall’s search API (see docstrings): e.g. unique="prints", order="released".
Exact and fuzzy name search
from pyscryfall import search_card_by_name_exact, search_card_by_name_fuzzy
# Exact match (case-insensitive)
card = search_card_by_name_exact("Lightning Bolt")
print(card.name)
# Fuzzy match (handles typos and partial names)
card = search_card_by_name_fuzzy("Lightn Bolt") # finds Lightning Bolt
print(card.name)
# Restrict to specific set
card = search_card_by_name_exact("Lightning Bolt", set_code="m21")
print(card.set_name)
Fetch all pages with pagination
from pyscryfall import search_cards_by_name_all
# Generator yields cards from all pages
for card in search_cards_by_name_all("Island", unique="prints"):
print(card.set_name, card.collector_number)
# Or collect all at once
cards = list(search_cards_by_name_all("Island", unique="prints"))
print(f"Found {len(cards)} total printings")
Fetch a single card by Scryfall ID
from pyscryfall import search_card_by_id, ScryfallCard
card: ScryfallCard = search_card_by_id("de652420-eacf-4f9d-9f13-c6bc02b0fa72")
print(card.name, card.type_line, card.oracle_text)
Random card
from pyscryfall import get_random_card
# Get any random card
card = get_random_card()
print(card.name)
# Get random card matching a query
creature = get_random_card(q="t:creature")
print(creature.type_line)
# Random legendary from a specific set
legendary = get_random_card(q="t:legendary set:war")
print(legendary.name)
Autocomplete card names
from pyscryfall import autocomplete_card_name
suggestions = autocomplete_card_name("Lightning")
print(suggestions.data) # ["Light", "Lightning Bolt", "Lightning Strike", ...]
# Include extra cards (tokens, etc.)
all_suggestions = autocomplete_card_name("Thopter", include_extras=True)
Card rulings
from pyscryfall import search_card_by_name_exact, get_card_rulings
card = search_card_by_name_exact("Humility")
rulings = get_card_rulings(card.id)
for ruling in rulings.data:
print(f"{ruling.published_at}: {ruling.comment}")
Handle API and HTTP errors
from pyscryfall import search_card_by_id, ScryfallApiError
try:
search_card_by_id("00000000-0000-0000-0000-000000000000")
except ScryfallApiError as exc:
print(exc)
print(exc.http_status)
if exc.body:
print(exc.body.code, exc.body.details)
Custom session or timeout
import requests
from pyscryfall import search_cards_by_name
session = requests.Session()
session.headers["User-Agent"] = "MyApp/1.0 (contact@example.com)"
cards = search_cards_by_name("Island", session=session, timeout=60.0)
Note: The library automatically sets a User-Agent header (pyscryfall/0.2.0), but you can override it by providing a custom session with your own User-Agent.
Serialize models
from pyscryfall import search_card_by_id
card = search_card_by_id("de652420-eacf-4f9d-9f13-c6bc02b0fa72")
payload = card.to_dict()
json_string = card.to_json()
# Deserialize
from pyscryfall import ScryfallCard
card = ScryfallCard.from_json_string(json_string)
For a stored JSON string of a list response, ScryfallCardList.from_json_string(s) and to_json() are available on the list type.
Rate limiting and API compliance
This library follows Scryfall's API guidelines:
Automatic rate limiting
The library automatically enforces Scryfall's 10 requests/second limit by adding a small delay between requests. This is transparent and requires no configuration.
Override the delay with the SCRYFALL_RATE_LIMIT_DELAY environment variable (in seconds):
export SCRYFALL_RATE_LIMIT_DELAY=0.15 # 150ms = ~6.7 req/sec
Rate limiting is shared across all API calls in the process.
User-Agent header
All requests automatically include a User-Agent: pyscryfall/0.2.0 header as required by Scryfall.
To use a custom User-Agent (e.g., for your application):
import requests
from pyscryfall import search_cards_by_name
session = requests.Session()
session.headers["User-Agent"] = "MyApp/1.0 (contact@example.com)"
cards = search_cards_by_name("Island", session=session)
Error handling
The library properly handles API error responses with structured error information:
from pyscryfall import search_card_by_name_exact, ScryfallApiError
try:
card = search_card_by_name_exact("NonexistentCard12345")
except ScryfallApiError as e:
print(f"HTTP Status: {e.http_status}")
if e.body:
print(f"Error Code: {e.body.code}")
print(f"Details: {e.body.details}")
Running tests
Tests live under tests/ and call the real Scryfall API, so they need network access.
With uv (from the repository root):
uv sync
uv run pytest
Verbose output:
uv run pytest -v
uv sync installs this project in editable mode so import pyscryfall works. Pytest is configured in pyproject.toml with testpaths = ["tests"] and pythonpath = ["."].
References
AI Disclosure
Part of this project has been developed with the help of an AI Model.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyscryfall-0.2.0.tar.gz.
File metadata
- Download URL: pyscryfall-0.2.0.tar.gz
- Upload date:
- Size: 41.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
760cb3844a66347642f2517948d8d81acf81fe525368513f1c18c73ca2765bdb
|
|
| MD5 |
eb732e97cc13093a5efb94929dc6b9fc
|
|
| BLAKE2b-256 |
b0c658ba71924cf8df4d9deee7e36d785caebd426130c00ec00bc2e28ff30ef3
|
File details
Details for the file pyscryfall-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pyscryfall-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b03fe51749379068989159fb6caf86f5ce79c292d92b74a94a4f736abd546af
|
|
| MD5 |
15089d8c49552fb0a8cf0a51ab88d23c
|
|
| BLAKE2b-256 |
289e4ae672f0ef82a883103c195216c95149658d06f8d6f2b43013663cabd000
|