This chapter brings together the concepts from all previous chapters in the form of complete, working Python examples. Each example targets a specific task: scanning an unknown tag, reading MIFARE Classic blocks, reading and writing NDEF, and debugging common errors.
All examples assume a PN532-based reader connected via USB or I2C, or an ACR122U. Hardware setup is covered in Chapter 7; library installation in Chapter 8.
Longer scripts are in the code/ folder (referenced by filename). Short illustrative snippets are shown inline.
Goal: Detect any tag, print its type, UID, ATQA, and SAK.
Full script: code/01_scan_tag.py
import nfc
def on_connect(tag):
print(f"Tag type : {tag.type}")
print(f"UID : {tag.identifier.hex().upper()}")
if hasattr(tag, 'atq'):
print(f"ATQA : {tag.atq.hex().upper()}")
if hasattr(tag, 'sak'):
print(f"SAK : {tag.sak:02X}")
return False # release tag immediately
with nfc.ContactlessFrontend('usb') as clf:
print("Hold a tag near the reader...")
clf.connect(rdwr={'on-connect': on_connect})
Sample output for an NTAG213:
Tag type : Type2Tag
UID : 04 6A B3 32 A5 63 80
ATQA : 0044
SAK : 00
Sample output for a MIFARE Classic 1K:
Tag type : MifareClassic
UID : A3 1F 9C 44
ATQA : 0004
SAK : 08
Goal: Authenticate to sector 0 with Key A and read block 1.
Full script: code/02_mifare_classic_read.py
import nfc
KEY_A = bytearray.fromhex('FFFFFFFFFFFF') # factory default
def on_connect(tag):
if tag.type != 'MifareClassic':
print("Not a MIFARE Classic tag")
return False
sector = 0
block = 1
try:
tag.authenticate(block, KEY_A, b'\x60') # 0x60 = key A
data = tag.read(block)
print(f"Block {block}: {data.hex()}")
except nfc.tag.TagCommandError as e:
print(f"Error: {e}")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Notes:
tag.authenticate(block_number, key, key_type) — key_type b'\x60' = Key A, b'\x61' = Key B.TagCommandError with status NAK is raised.Goal: Try a list of common keys on each sector and dump readable sectors.
Full script: code/03_mifare_classic_dump.py
import nfc
COMMON_KEYS = [
bytes.fromhex('FFFFFFFFFFFF'),
bytes.fromhex('A0A1A2A3A4A5'),
bytes.fromhex('D3F7D3F7D3F7'),
bytes.fromhex('000000000000'),
bytes.fromhex('B0B1B2B3B4B5'),
]
def try_read_sector(tag, sector, keys):
first_block = sector * 4
for key in keys:
for key_type, code in [(bytearray(key), b'\x60'),
(bytearray(key), b'\x61')]:
try:
tag.authenticate(first_block, key_type, code)
blocks = [tag.read(first_block + i).hex()
for i in range(4)]
return blocks, key.hex(), 'A' if code == b'\x60' else 'B'
except Exception:
continue
return None, None, None
def on_connect(tag):
if tag.type != 'MifareClassic':
return False
print(f"UID: {tag.identifier.hex()}")
for sector in range(16):
blocks, key, key_id = try_read_sector(tag, sector, COMMON_KEYS)
if blocks:
print(f"Sector {sector:2d} (Key {key_id}={key}):")
for i, b in enumerate(blocks):
print(f" Block {sector*4+i}: {b}")
else:
print(f"Sector {sector:2d}: LOCKED (no matching key found)")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Goal: Read and print the NDEF message from an NTAG213 sticker.
import nfc
def on_connect(tag):
if not tag.ndef:
print("No NDEF data found (tag may not be NDEF-formatted)")
return False
print(f"NDEF version : {tag.ndef.version}")
print(f"NDEF capacity : {tag.ndef.capacity} bytes")
print(f"NDEF length : {tag.ndef.length} bytes")
print(f"Writeable : {tag.ndef.is_writeable}")
print()
for i, record in enumerate(tag.ndef.records):
print(f"Record {i}: {record}")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Sample output for an NTAG213 with a URL:
NDEF version : 1.0
NDEF capacity : 137 bytes
NDEF length : 15 bytes
Writeable : True
Record 0: UriRecord('https://example.com')
Goal: Write https://example.com to a blank or already-formatted NTAG213.
import nfc, ndef
TARGET_URL = "https://example.com"
def on_connect(tag):
if tag.ndef is None:
print("Tag is not NDEF-formatted. Attempting format...")
tag.format()
records = [ndef.UriRecord(TARGET_URL)]
tag.ndef.records = records
print(f"Wrote NDEF URI: {TARGET_URL}")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Notes:
tag.format() writes the Capability Container and initialises the TLV structure.tag.ndef.records = [...] encodes the records and writes them in a single operation.Full script with error handling: code/05_write_ndef_uri.py
Writing NDEF to MIFARE Classic requires MAD setup. This is more involved: you need to write the MAD sector (sector 0) with the correct AID, then write NDEF TLV content in the NDEF sectors.
nfcpy handles this through tag.format() which writes MAD1 or MAD2 as appropriate:
import nfc, ndef
def on_connect(tag):
if tag.type != 'MifareClassic':
return False
mad_key = bytearray.fromhex('A0A1A2A3A4A5')
ndef_key = bytearray.fromhex('D3F7D3F7D3F7')
# Format creates MAD with default keys
tag.format(wipe=0x00)
records = [ndef.UriRecord("https://example.com")]
tag.ndef.records = records
print("NDEF written to MIFARE Classic via MAD")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Note: tag.format() on MIFARE Classic writes the NDEF key (D3F7D3F7D3F7) to Key A of all NDEF sectors. This is the standard NFC Forum key for MIFARE Classic NDEF. The MAD key (A0A1A2A3A4A5) is written to sector 0 Key A. After formatting, the card is only accessible with these standard keys.
Goal: Read all user pages directly (bypassing NDEF), useful for debugging.
Full script: code/07_ntag_dump.py
import nfc
def on_connect(tag):
if tag.type != 'Type2Tag':
return False
print(f"UID: {tag.identifier.hex()}")
# Type2Tag exposes read(page) returning 16 bytes (4 pages)
for page in range(0, 45, 4):
data = tag.read(page)
for i in range(4):
offset = i * 4
row = page + i
print(f" Page {row:3d}: {data[offset:offset+4].hex()}")
return False
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
Sample output (pages 0–7):
UID: 046ab332a56380
Page 0: 04 6a b3 cd
Page 1: 32 a5 63 80
Page 2: a8 48 00 00
Page 3: e1 10 12 00 ← Capability Container
Page 4: 03 0f d1 01 ← NDEF TLV start
Page 5: 0b 55 04 65
Page 6: 78 61 6d 70
Page 7: 6c 65 2e 63
Goal: Use pyscard (PC/SC) to get the UID of any tag.
from smartcard.System import readers
from smartcard.util import toHexString, toBytes
r = readers()
if not r:
print("No readers found")
exit()
print(f"Using reader: {r[0]}")
conn = r[0].createConnection()
conn.connect()
# ACR122U GET UID pseudo-APDU
get_uid = [0xFF, 0xCA, 0x00, 0x00, 0x00]
data, sw1, sw2 = conn.transmit(get_uid)
if sw1 == 0x90:
print(f"UID: {toHexString(data)}")
else:
print(f"Error: SW={sw1:02X}{sw2:02X}")
conn.disconnect()
Symptoms: TagCommandError: [NAK], authentication failure, write failure.
Causes and fixes:
Symptoms: Tag detected but immediately lost; inconsistent reads.
Causes and fixes:
nfc.clf.device.Error: [Errno 13] Permission denied
Fix (Linux): Add udev rule or add your user to the plugdev group:
sudo usermod -aG plugdev $USER
# then log out and back in
If both pcscd and a libnfc-based tool try to claim the same USB reader:
sudo service pcscd stop # temporarily stop PC/SC daemon
# ... run libnfc tool ...
sudo service pcscd start # restart
Or permanently blacklist the ACR122U from pcscd by adding it to /etc/modprobe.d/blacklist.conf:
blacklist pn533
blacklist nfc
Symptoms: tag.ndef is None despite the tag being “NFC-enabled”.
Causes:
0xE1 → write correct CC manually0x10 → write E1 10 [size] 000xFE after NDEF TLVCheck:
0x00 = R/W; 0xFF = read-only0x00 0x00 0x00 = unlocked; 0xFF 0xFF 0xFF = fully lockedGoal: Build an NDEF message by hand, encode to bytes, parse back.
import ndef
# Build a multi-record Smart Poster: URL + title
records = [
ndef.SmartposterRecord(
resource="https://example.com",
title=[("en", "Example website"), ("fr", "Site exemple")],
action="open",
)
]
encoded = b"".join(ndef.message_encoder(records))
print(f"Encoded ({len(encoded)} bytes): {encoded.hex()}")
# Parse back
for record in ndef.message_decoder(encoded):
print(f"Type: {type(record).__name__}")
print(f" URI: {record.resource}")
for lang, text in record.title:
print(f" Title [{lang}]: {text}")
ContactlessFrontend with a callback is the standard pattern for all Python NFC work.tag.ndef.records provides direct NDEF read/write without manual TLV handling.tag.authenticate(block, key, key_type) before tag.read / tag.write.tag.read(page) which returns 16 bytes (4 pages at a time).FF CA 00 00 00 is the easiest way to get a UID from an ACR122U.| ← Chapter 8: Software Libraries | Table of Contents | Chapter 10: Action Plan → |