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.
pip install pyfallfrom 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)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"Pyfall supports ~99% of Scryfall's search syntax. The canonical reference is Scryfall's syntax documentation.
| 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 |
| 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 |
- 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
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)
| 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 |
even/odd: Parity check for CMC (cmc:even)*: Variable power/toughness (pow:*)
| Syntax | Description | Example |
|---|---|---|
cn: / number: |
Collector number | cn:100, cn>=50 |
| Range syntax | Number range | cn:100-200 |
| 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 |
| 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 |
Pyfall supports all Scryfall is: properties:
is:legendary, is:creature, is:instant, is:sorcery, is:artifact, is:enchantment, is:planeswalker, is:land, is:battle, is:tribal, is:kindred
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
is:spell, is:permanent, is:historic, is:vanilla, is:frenchvanilla, is:bear, is:modal, is:party, is:outlaw, is:manland
is:commander, is:brawler, is:companion, is:partner, is:reserved
is:hybrid, is:phyrexian
is:foil, is:nonfoil, is:promo, is:fullart, is:textless, is:reprint, is:firstprint, is:digital, is:oversized, is:hires, is:spotlight, is:booster
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
is:funny, is:etched, is:glossy, is:alchemy, is:rebalanced, is:universesbeyond, is:colorshifted, is:datestamped
| 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 |
| 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}
| Syntax | Description | Example |
|---|---|---|
frame: |
Frame style | frame:2015, frame:future |
border: |
Border color | border:black, border:borderless |
stamp: |
Security stamp | stamp:oval, stamp:acorn |
| Syntax | Description | Example |
|---|---|---|
game: |
Game availability | game:paper, game:arena, game:mtgo |
| 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).
| 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) |
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.
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.
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,
)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}")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 regexOr 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
}
)from pyfall.backends import SQLBackend, mysql_regex
from pyfall import RegexFilter
backend = SQLBackend(
node_compilers={
RegexFilter: mysql_regex,
}
)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,
}
)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,
}
)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)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)
# )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']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')}AndGroup- Logical AND of childrenOrGroup- Logical OR of childrenNotFilter- Negation
NameFilter- Card nameTypeFilter- Type lineOracleFilter- Oracle textFullOracleFilter- Full oracle (with reminder)FlavorFilter- Flavor textKeywordFilter- Keywords
ColorFilter- Card colorsColorIdentityFilter- Color identityProducesFilter- Mana productionDevotionFilter- Devotion count
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
SetFilter- Set codeBlockFilter- Block codeSetTypeFilter- Set typeRarityFilter- Rarity
FormatFilter- Format legality
IsFilter- Card propertiesHasFilter- Has propertiesManaCostFilter- Mana cost patternIncludeFilter- Include extras
ArtistFilter- Artist nameLanguageFilter- LanguageWatermarkFilter- WatermarkFrameFilter- Frame styleBorderFilter- Border colorGameFilter- Game availabilityCubeFilter- Cube membershipStampFilter- Security stampInFilter- Ever printed inArtTagFilter- Art tagsOracleTagFilter- Oracle tagsNewFilter- New property
UniqueFilter- Result groupingOrderFilter- Sort orderPreferFilter- Print preferenceCheapestFilter- Price preferenceDisplayFilter- Display mode
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})
RegexFilter- Regex pattern match
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 |
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.
| 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) |
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.
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.
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}
)# 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/pyfallMIT License - see LICENSE for details.
- Scryfall for their excellent search syntax and API
- The Magic: The Gathering community