Python NFT Metadata Generation — Core Concepts

Why metadata matters

NFT metadata determines what wallets, marketplaces, and collectors actually see. OpenSea, Rarible, and other platforms read metadata files to display names, images, descriptions, and traits. If your metadata is malformed, your NFTs appear as blank cards — worthless regardless of the art behind them.

The metadata standard

Most NFT projects follow the ERC-721 or ERC-1155 metadata standard. A typical metadata file is JSON:

{
  "name": "Cosmic Robot #437",
  "description": "A rare cosmic robot from the Neon Galaxy collection.",
  "image": "ipfs://QmXyz.../437.png",
  "attributes": [
    {"trait_type": "Background", "value": "Nebula"},
    {"trait_type": "Body", "value": "Chrome"},
    {"trait_type": "Hat", "value": "Gold Crown"},
    {"trait_type": "Eyes", "value": "Laser"},
    {"trait_type": "Rarity Score", "display_type": "number", "value": 87}
  ]
}

Key fields:

  • name: Displayed as the NFT title
  • image: URL to the visual (usually IPFS or Arweave for permanence)
  • attributes: Array of trait objects that marketplaces use for filtering and rarity calculations

Trait system design

Before writing code, you define the trait categories and their distribution:

Trait CategoryOptionsRarity Distribution
Background8 colorsCommon: 60%, Uncommon: 30%, Rare: 10%
Body5 typesEqual distribution
Hat12 stylesWeighted by rarity tier
Eyes10 styles3 legendary (1% each), rest distributed
Accessory15 items + “None”40% have no accessory

The rarity system creates value differentiation. Some trait combinations should be extremely rare, which drives collector interest.

Generating unique combinations

Python’s random module with weighted choices creates the trait combinations:

import random
from collections import Counter

def generate_traits(trait_config, count, seed=42):
    random.seed(seed)  # Reproducible generation
    collection = []
    seen = set()

    while len(collection) < count:
        traits = {}
        for category, options in trait_config.items():
            values = [o["value"] for o in options]
            weights = [o["weight"] for o in options]
            traits[category] = random.choices(values, weights=weights, k=1)[0]

        trait_key = tuple(sorted(traits.items()))
        if trait_key not in seen:
            seen.add(trait_key)
            collection.append(traits)

    return collection

Setting a seed ensures the same collection is generated every time — critical for auditing and reproducing builds.

Image compositing

Each trait corresponds to a PNG layer with transparency. Python’s Pillow library stacks them:

from PIL import Image

def compose_image(layers_dir, traits, size=(2048, 2048)):
    base = Image.new("RGBA", size, (0, 0, 0, 0))
    layer_order = ["Background", "Body", "Eyes", "Hat", "Accessory"]

    for category in layer_order:
        trait_value = traits.get(category)
        if trait_value and trait_value != "None":
            layer_path = layers_dir / category / f"{trait_value}.png"
            layer = Image.open(layer_path).resize(size)
            base = Image.alpha_composite(base, layer)

    return base

Layer order matters — background goes first, accessories last. Each layer file must have consistent dimensions and transparent backgrounds.

Validation before upload

Before uploading thousands of files, validate everything:

  • Uniqueness: No two tokens share the exact same trait combination.
  • Completeness: Every referenced image file exists.
  • Schema compliance: Every JSON file matches the expected structure.
  • Trait consistency: Actual rarity distributions match the intended design (within statistical tolerance).
  • Image quality: Spot-check that layers composite correctly without artifacts.

Storage considerations

Metadata and images need to be stored somewhere the blockchain contract can reference them. Common options:

  • IPFS: Decentralized, content-addressed. Files are permanent once pinned. Most NFT projects use IPFS.
  • Arweave: Permanent storage with one-time payment. More expensive upfront but no ongoing pinning costs.
  • Centralized servers: Fast but risky — if the server goes down, NFTs lose their content.

The smart contract typically stores a base URI like ipfs://QmBaseHash/, and each token appends its ID: ipfs://QmBaseHash/437.json.

Common misconception

Some developers generate metadata after minting, which creates a gap where NFTs exist on-chain but display as blank. Always generate and upload all metadata before deploying the contract, and verify that the base URI resolves correctly for every token ID.

One thing to remember

NFT metadata generation is a data pipeline — define traits, generate unique combinations with controlled rarity, composite images from layers, validate everything, upload to permanent storage, and only then deploy the contract that points to them.

pythonblockchainnft

See Also