A Scryfall search syntax parser for Magic: The Gathering
Project description
Pyfall
A Python library for parsing Scryfall search syntax into an Abstract Syntax Tree (AST).
Pyfall lets you parse Scryfall's powerful card search syntax and compile it to SQL or other query languages for searching your own card database.
Pyfall is not affiliated with, endorsed by, or sponsored by Scryfall.
Installation
pip install pyfall
Quick Start
from pyfall import parse
from pyfall.backends import SQLBackend
# Parse a Scryfall query
ast = parse("t:creature c:green cmc<=3")
# Compile to SQL
backend = SQLBackend()
sql, params = backend.compile(ast)
# Use with your database
cursor.execute(f"SELECT * FROM cards WHERE {sql}", params)
CLI Usage
Pyfall includes a command-line interface for testing queries:
# Parse a query and show the AST
python -m pyfall parse "t:creature c:green"
# Compile a query to SQL
python -m pyfall sql "t:creature c:green"
# Validate a query
python -m pyfall validate "t:creature c:green"
Supported Syntax
Pyfall supports ~99% of Scryfall's search syntax. The canonical reference is Scryfall's syntax documentation.
Text Filters
| Syntax | Description | Example |
|---|---|---|
name (plain text) |
Card name | lightning bolt |
t: / type: |
Type line | t:creature, t:"legendary artifact" |
o: / oracle: |
Oracle text | o:draw, o:"enters the battlefield" |
fo: / fulloracle: |
Full oracle text (includes reminder) | fo:flying |
kw: / keyword: |
Keyword abilities | kw:flying, kw:trample |
ft: / flavor: |
Flavor text | ft:chandra |
a: / artist: |
Artist name | a:"Rebecca Guay" |
wm: / watermark: |
Watermark | wm:phyrexian |
Color Filters
| Syntax | Description | Example |
|---|---|---|
c: / color: |
Card colors | c:w, c:wu, c:colorless |
id: / identity: |
Color identity | id:wubrg, id:c |
produces: |
Mana production | produces:g, produces:wu |
devotion: |
Devotion (mana pips) | devotion:ww, devotion:uuu |
Color Values
- Single letters:
w(white),u(blue),b(black),r(red),g(green) - Colorless:
corcolorless - Multicolor:
mormulticolor - Guild nicknames:
azorius,dimir,rakdos,gruul,selesnya,orzhov,izzet,golgari,boros,simic - Shard/Wedge nicknames:
bant,esper,grixis,jund,naya,abzan,jeskai,sultai,mardu,temur
Color Comparison Modes
c:wu # Has at least white AND blue (may have more colors)
c=wu # Has exactly white and blue (no other colors)
c<=wu # Colors are subset of white/blue (could be W, U, WU, or colorless)
c>=wu # Has at least white and blue (same as c:wu)
Numeric Filters
| Syntax | Description | Example |
|---|---|---|
cmc: / mv: / manavalue: |
Mana value | cmc:3, cmc<=2, mv>=5 |
pow: / power: |
Power | pow:4, pow>=3, pow:* |
tou: / toughness: |
Toughness | tou:5, tou<3 |
pt: / powtou: |
Power + Toughness | pt:6 |
loy: / loyalty: |
Loyalty | loy:4 |
def: / defense: |
Defense (battles) | def:5 |
usd: / eur: / tix: |
Price | usd<1, eur>=10 |
year: |
Release year | year:2023, year>=2020 |
date: |
Release date | date>=2023-01-01, date:now |
artists: |
Artist count | artists:2 |
illustrations: |
Illustration count | illustrations>=2 |
Special Numeric Values
even/odd: Parity check for CMC (cmc:even)*: Variable power/toughness (pow:*)
Collector Number
| Syntax | Description | Example |
|---|---|---|
cn: / number: |
Collector number | cn:100, cn>=50 |
| Range syntax | Number range | cn:100-200 |
Set & Rarity
| Syntax | Description | Example |
|---|---|---|
set: / s: / e: |
Set code | set:neo, s:mh2 |
b: / block: |
Block code | b:zendikar |
st: |
Set type | st:core, st:expansion |
r: / rarity: |
Rarity | r:mythic, r:r, r>=uncommon |
in: |
Ever printed in/at | in:rare, in:paper, in:ja |
Format Legality
| Syntax | Description | Example |
|---|---|---|
f: / format: / legal: |
Legal in format | f:commander, f:modern |
banned: |
Banned in format | banned:legacy |
restricted: |
Restricted in format | restricted:vintage |
Card Properties (is:)
Pyfall supports all Scryfall is: properties:
Type Properties
is:legendary, is:creature, is:instant, is:sorcery, is:artifact, is:enchantment, is:planeswalker, is:land, is:battle, is:tribal, is:kindred
Layout Properties
is:split, is:flip, is:transform, is:meld, is:leveler, is:dfc, is:mdfc, is:tdfc, is:adventure, is:saga, is:class, is:prototype, is:case
Card Characteristics
is:spell, is:permanent, is:historic, is:vanilla, is:frenchvanilla, is:bear, is:modal, is:party, is:outlaw, is:manland
Format Properties
is:commander, is:brawler, is:companion, is:partner, is:reserved
Mana Properties
is:hybrid, is:phyrexian
Print Properties
is:foil, is:nonfoil, is:promo, is:fullart, is:textless, is:reprint, is:firstprint, is:digital, is:oversized, is:hires, is:spotlight, is:booster
Land Cycle Shortcuts
is:dual, is:fetchland, is:shockland, is:checkland, is:fastland, is:slowland, is:painland, is:filterland, is:triland, is:triome, is:canopyland, is:bikeland, is:bondland, is:bounceland, is:gainland, is:pathway, is:scryland, is:surveilland, is:shadowland, is:storageland, is:tangoland, is:creatureland
Other Properties
is:funny, is:etched, is:glossy, is:alchemy, is:rebalanced, is:universesbeyond, is:colorshifted, is:datestamped
Has Properties (has:)
| Syntax | Description |
|---|---|
has:indicator |
Has color indicator |
has:watermark |
Has watermark |
has:foil |
Available in foil |
has:nonfoil |
Available in nonfoil |
has:promo |
Is a promo |
has:colors |
Has colors (not colorless) |
has:flavor |
Has flavor text |
has:oracle |
Has oracle text |
has:loyalty |
Has loyalty (planeswalker) |
has:pt |
Has power/toughness |
has:multifaced |
Has multiple faces |
Mana Cost
| Syntax | Description | Example |
|---|---|---|
m: / mana: |
Mana cost pattern | m:{2}{W}{W}, m:{G/U} |
Supported mana symbols:
- Colored:
{W},{U},{B},{R},{G} - Generic:
{1},{2},{X},{Y} - Colorless:
{C} - Snow:
{S} - Hybrid:
{W/U},{2/W} - Phyrexian:
{W/P},{U/P}
Frame & Border
| Syntax | Description | Example |
|---|---|---|
frame: |
Frame style | frame:2015, frame:future |
border: |
Border color | border:black, border:borderless |
stamp: |
Security stamp | stamp:oval, stamp:acorn |
Game Availability
| Syntax | Description | Example |
|---|---|---|
game: |
Game availability | game:paper, game:arena, game:mtgo |
Regex Support
| Syntax | Description | Example |
|---|---|---|
name:/pattern/ |
Name regex | name:/^Lightning/ |
type:/pattern/ |
Type regex | type:/Goblin.*Warrior/ |
oracle:/pattern/ |
Oracle regex | oracle:/\{T\}:.*damage/ |
flavor:/pattern/ |
Flavor regex | flavor:/fire/ |
Note: Regex patterns are converted to SQL LIKE patterns by default. For true regex support, use database-specific backends (see below).
Logical Operators
| Syntax | Description | Example |
|---|---|---|
| (space) | AND (implicit) | t:creature c:green |
or |
OR | t:creature or t:artifact |
- |
NOT (negation) | -t:creature, -c:red |
() |
Grouping | c:w (t:creature or t:enchantment) |
Display Directives
These operators parse successfully but affect result presentation rather than filtering:
| Syntax | Description | Example |
|---|---|---|
unique: |
Result grouping | unique:cards, unique:prints, unique:art |
order: |
Sort field | order:cmc, order:name, order:usd |
direction: |
Sort direction | direction:asc, direction:desc |
prefer: |
Print preference | prefer:oldest, prefer:usd-low |
include: |
Include extras | include:extras |
cheapest: |
Price preference | cheapest:usd |
display: |
Display mode | display:grid, display:images |
Note: Display directives return 1=1 from the SQL backend since they don't filter results. Use extract_directives() to handle them in your application layer.
Stub Filters (Require External Data)
These filters parse correctly but require external data not in Scryfall bulk downloads:
| Syntax | Notes |
|---|---|
cube: |
Requires Scryfall cube API data |
art: / atag: / arttag: |
Requires Scryfall art tag data |
function: / otag: / oracletag: |
Requires Scryfall oracle tag data |
new: |
Requires printing history analysis |
Enable strict_mode=True to raise UnsupportedFilterError instead of silently matching all cards.
Backend Configuration
Column Mapping
Map Scryfall field names to your database columns:
from pyfall.backends import SQLBackend
backend = SQLBackend(
column_map={
'name': 'card_name',
'type': 'card_type_line',
'oracle': 'rules_text',
'colors': 'card_colors',
'cmc': 'mana_value',
},
table_name='my_cards',
case_insensitive=True,
)
Strict Mode
Enable strict mode to raise errors on stub filters instead of silently matching all:
backend = SQLBackend(strict_mode=True)
try:
sql, params = backend.compile(parse("cube:vintage"))
except UnsupportedFilterError as e:
print(f"Filter not supported: {e}")
Database-Specific Features
PostgreSQL
from pyfall.backends import create_postgres_backend
# Factory creates backend with PostgreSQL regex support
backend = create_postgres_backend()
ast = parse("name:/^Lightning/")
sql, params = backend.compile(ast)
# Uses ~* operator for case-insensitive regex
Or configure manually:
from pyfall.backends import SQLBackend, postgres_regex
from pyfall import RegexFilter
backend = SQLBackend(
node_compilers={
RegexFilter: postgres_regex, # Case-insensitive
# Or: postgres_regex_case_sensitive for case-sensitive
}
)
MySQL
from pyfall.backends import SQLBackend, mysql_regex
from pyfall import RegexFilter
backend = SQLBackend(
node_compilers={
RegexFilter: mysql_regex,
}
)
SQLite
from pyfall.backends import SQLBackend, sqlite_regex
from pyfall import RegexFilter
# Note: Requires REGEXP function to be loaded in SQLite
backend = SQLBackend(
node_compilers={
RegexFilter: sqlite_regex,
}
)
Custom Node Compilers
Override compilation for any filter type:
from pyfall.backends import SQLBackend
from pyfall import CubeFilter, IsFilter
def my_cube_filter(node, backend):
"""Look up cube membership in your own table."""
return (
"id IN (SELECT card_id FROM cube_cards WHERE cube = ?)",
[node.value]
)
def my_fetchland_check(node, backend):
"""Use a curated list of fetchland oracle IDs."""
if node.property == 'fetchland':
return (
"oracle_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
# Onslaught fetches
"a1b2c3...", "d4e5f6...",
# Zendikar fetches
"g7h8i9...", "j0k1l2...",
# etc.
]
)
# Fall back to default for other is: properties
return backend._compile_is(node)
backend = SQLBackend(
node_compilers={
CubeFilter: my_cube_filter,
IsFilter: my_fetchland_check,
}
)
Utility Functions
Walking the AST
from pyfall import parse, walk, find_all, TypeFilter, ColorFilter
ast = parse("t:creature c:green or t:artifact")
# Iterate all nodes
for node in walk(ast):
print(type(node).__name__)
# Find specific node types
type_filters = find_all(ast, TypeFilter)
color_filters = find_all(ast, ColorFilter)
Pretty Printing
from pyfall import parse, pretty_print
ast = parse("t:creature c:green cmc<=3")
print(pretty_print(ast))
# Output:
# AndGroup(
# TypeFilter(value='creature')
# ColorFilter(colors=['G'], mode='at_least')
# CMCFilter(op=LE, value=3.0)
# )
Query Validation
from pyfall import parse, validate
result = validate("t:creature c:green")
print(result.is_valid) # True
print(result.filters_used) # {TypeFilter, ColorFilter}
print(result.has_directives) # False
print(result.stub_filters) # set()
result = validate("cube:vintage")
print(result.stub_filters) # {CubeFilter}
print(result.warnings) # ['cube: requires external data']
Extracting Display Directives
from pyfall import parse, extract_directives
ast = parse("t:creature unique:art order:name")
filters, directives = extract_directives(ast)
# filters: AndGroup with just TypeFilter
# directives: {'unique': UniqueFilter(mode='art'), 'order': OrderFilter(field='name')}
AST Node Types
Logical Nodes
AndGroup- Logical AND of childrenOrGroup- Logical OR of childrenNotFilter- Negation
Text Filters
NameFilter- Card nameTypeFilter- Type lineOracleFilter- Oracle textFullOracleFilter- Full oracle (with reminder)FlavorFilter- Flavor textKeywordFilter- Keywords
Color Filters
ColorFilter- Card colorsColorIdentityFilter- Color identityProducesFilter- Mana productionDevotionFilter- Devotion count
Numeric Filters
CMCFilter- Mana valuePowerFilter- PowerToughnessFilter- ToughnessPowerToughnessFilter- P+T combinedLoyaltyFilter- LoyaltyDefenseFilter- DefensePriceFilter- Price (usd/eur/tix)CollectorNumberFilter- Collector numberPrintsFilter- Print countDateFilter- Release dateArtistCountFilter- Artist countIllustrationsFilter- Illustration countYearFilter- Release year
Set/Rarity
SetFilter- Set codeBlockFilter- Block codeSetTypeFilter- Set typeRarityFilter- Rarity
Format
FormatFilter- Format legality
Properties
IsFilter- Card propertiesHasFilter- Has propertiesManaCostFilter- Mana cost patternIncludeFilter- Include extras
Metadata
ArtistFilter- Artist nameLanguageFilter- LanguageWatermarkFilter- WatermarkFrameFilter- Frame styleBorderFilter- Border colorGameFilter- Game availabilityCubeFilter- Cube membershipStampFilter- Security stampInFilter- Ever printed inArtTagFilter- Art tagsOracleTagFilter- Oracle tagsNewFilter- New property
Display/Sort (Directives)
UniqueFilter- Result groupingOrderFilter- Sort orderPreferFilter- Print preferenceCheapestFilter- Price preferenceDisplayFilter- Display mode
Mana Symbols
ManaSymbol- Base classGenericMana- Generic mana ({1}, {X})ColoredMana- Colored mana ({W}, {U})HybridMana- Hybrid mana ({W/U})PhyrexianMana- Phyrexian mana ({W/P})ColorlessMana- Colorless mana ({C})SnowMana- Snow mana ({S})
Regex
RegexFilter- Regex pattern match
Known Semantic Differences from Scryfall
Some filters have different semantics than Scryfall:
| Filter | Scryfall | Pyfall Default |
|---|---|---|
in:rare |
Ever printed at rare (any printing) | Current printing is rare |
in:m / in:r |
Treats as set code (no match) | Treats as rarity abbreviation |
is:dual |
Exact list of 10 ABUR duals | Pattern match on type line |
is:fetchland |
Curated list | Pattern match on oracle text |
name:/pattern/ |
True regex | Converted to LIKE (approximate) |
m:{Q} |
Error: "Unknown mana symbols" | Accepts as GenericMana |
c:xyz |
Error: "Unknown color" | Accepts with empty color list |
cmc:abc |
Error: "Value must be a number" | Falls back to 0.0 |
in: Filter Disambiguation
Scryfall disambiguates in: values in this order:
- Full rarity names only:
in:rare,in:mythic,in:uncommonwork;in:r,in:mdo not expand to rarities - Game platforms:
in:paper,in:mtgo,in:arena - Language codes:
in:ru,in:en,in:ja, etc. (2-3 letter ISO codes) - Set codes: Any other value is treated as a set code
Pyfall currently treats single-letter rarity abbreviations (m, r, c, u) as rarities, which differs from Scryfall.
Color Filter Defaults
| Filter | Default Mode |
|---|---|
c:wu |
AT_LEAST (has at least W and U, may have more) |
id:wu |
AT_MOST (identity is subset of WU) |
c=wu |
EXACT (has exactly W and U) |
c<=wu |
AT_MOST (colors are subset of WU) |
c>=wu |
AT_LEAST (same as c:wu) |
Silent Normalizations
Pyfall accepts some inputs that Scryfall rejects:
- Invalid mana symbols:
m:{Q}becomesGenericMana(value='Q')instead of an error - Invalid colors:
c:xyzresults in an empty color filter instead of an error - Non-numeric CMC:
cmc:abcfalls back to 0.0 instead of an error
For stricter validation matching Scryfall's behavior, consider adding input validation before parsing.
Recommended: Use strict_mode for Production
For production use, enable strict_mode to get errors on stub filters that require external data:
from pyfall import parse, SQLBackend, UnsupportedFilterError
backend = SQLBackend(strict_mode=True)
try:
sql, params = backend.compile(parse("cube:vintage"))
except UnsupportedFilterError as e:
print(f"Filter not supported: {e}")
For exact Scryfall semantics on in: filters, you'll need a printings table with historical data. For is:dual/is:fetchland, use node_compilers to inject curated card lists.
Format Legality Schema
The default FormatFilter compilation assumes a JSON column structure:
{"commander": "legal", "modern": "banned", "vintage": "restricted"}
For different schemas (e.g., separate legalities table), override with node_compilers:
def my_format_filter(node, backend):
"""Check format legality in normalized table."""
return (
"id IN (SELECT card_id FROM legalities WHERE format = ? AND status = ?)",
[node.value, node.status]
)
backend = SQLBackend(
node_compilers={FormatFilter: my_format_filter}
)
Development
# Clone the repo
git clone https://github.com/PhilMcClelland/pyfall.git
cd pyfall
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=pyfall
# Type checking
mypy src/pyfall
# Linting
ruff check src/pyfall
License
MIT License - see LICENSE for details.
Acknowledgments
- Scryfall for their excellent search syntax and API
- The Magic: The Gathering community
Project details
Release history Release notifications | RSS feed
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 pyfall_mtg-0.1.0.tar.gz.
File metadata
- Download URL: pyfall_mtg-0.1.0.tar.gz
- Upload date:
- Size: 74.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e007e7e5ba1019e4150645a69d2b31a35d2e38118e18045209f57592f778a86
|
|
| MD5 |
f1767a6b510715386ff64c92adf89070
|
|
| BLAKE2b-256 |
b9fc0ee13f722c20a19bfe65aab600c1ddb194726a6fb8ed2355c0c31c84f8f7
|
File details
Details for the file pyfall_mtg-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyfall_mtg-0.1.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.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa43ec241e3d470ab07691bfbdc468b09923ffccd2b6f9a1f929723deba1a98a
|
|
| MD5 |
5a365b1dae99bd098f371a59f9075225
|
|
| BLAKE2b-256 |
c6952de6b868fa4d03c4cde72f14a138d4c010982463a95c6a9106b3265ce96a
|