How to Read a Smart Card with Python (pyscard)
This tutorial walks you through everything you need to read smart cards from Python: installing pyscard, listing readers, connecting to cards, sending APDU commands, parsing ATR, and monitoring card events. Works on Windows, macOS, and Linux with any PC/SC reader (ACR122U, HID Omnikey, Identiv, etc.).
1. Install pyscard and pcsc-lite
Windows
pip install pyscard
No additional setup needed — Windows has the PC/SC subsystem built-in (winscard.dll).
Linux (Ubuntu/Debian)
sudo apt update sudo apt install pcscd pcsc-tools libpcsclite-dev pip install pyscard sudo systemctl start pcscd
macOS
pip install pyscard # PC/SC framework built-in on macOS
python -c "import smartcard; print('OK')" should print OK without errors.2. List Connected Smart Card Readers
from smartcard.System import readers
# Get all connected readers
r = readers()
if not r:
print("No readers found. Is your reader plugged in?")
exit(1)
for i, reader in enumerate(r):
print(f"Reader {i}: {reader}")
Output example:
Reader 0: ACS ACR122U PICC Interface 00 00 Reader 1: HID OMNIKEY 5022 Smart Card Reader 0
3. Connect to a Card and Get ATR
from smartcard.System import readers
r = readers()
connection = r[0].createConnection()
connection.connect()
# ATR is available after connect()
print(f"ATR: {connection.getATR():02X}")
print(f"Protocol: {connection.getProtocol()}")
# ATRIBUTE_ATR_STRING = 0x00DE is not needed — getATR() does it
Parse the ATR
The ATR (Answer to Reset) tells you the card's protocol, data rate, and historical bytes. You can decode ATR hex manually or use our ATR Decoder Tool for a full breakdown.
# Quick ATR parsing
atr = connection.getATR()
ts = atr[0] # TS byte: 0x3B = direct convention, 0x23 = inverse
t0 = atr[1] # T0: Y1(high nibble) + K(low nibble)
print(f"Convention: {'Direct' if ts == 0x3B else 'Inverse'}")
print(f"Historical bytes: {t0 & 0x0F}")
4. Send Your First APDU — SELECT
To read data from a card, you first select its application (DF) or root directory (MF).
from smartcard.util import toHexString
# SELECT MF (Master File, 3F00) — works on most cards
SELECT_MF = [0x00, 0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00]
response, sw1, sw2 = connection.transmit(SELECT_MF)
print(f"Response data: {toHexString(response)}")
print(f"Status Word: {sw1:02X} {sw2:02X}")
if sw1 == 0x90 and sw2 == 0x00:
print("MF selected — card is ready")
elif sw1 == 0x61:
# T=0 card: use GET RESPONSE to fetch remaining data
print(f"T=0: {sw2} bytes available, send GET RESPONSE")
else:
print(f"Error: SW1={sw1:02X} SW2={sw2:02X}")
5. Read a File — READ BINARY Example
# Read ICCID from a SIM card (EF at 0x2FE2)
SELECT_ICCID = [0x00, 0xA4, 0x00, 0x00, 0x02, 0x2F, 0xE2]
_, sw1, sw2 = connection.transmit(SELECT_ICCID)
if sw1 == 0x90 and sw2 == 0x00:
# File selected — read 10 bytes
READ_BINARY = [0x00, 0xB0, 0x00, 0x00, 0x0A]
data, sw1, sw2 = connection.transmit(READ_BINARY)
if sw1 == 0x90:
print(f"ICCID: {toHexString(data)}")
elif sw1 == 0x61:
# T=0: data available via GET RESPONSE
GET_RESPONSE = [0x00, 0xC0, 0x00, 0x00, sw2]
data, sw1, sw2 = connection.transmit(GET_RESPONSE)
print(f"ICCID: {toHexString(data)}")
6. Handle T=0 GET RESPONSE Properly
The most common gotcha. When a T=0 card responds with 61 XX, you must send a separate GET RESPONSE to retrieve the data.
def transmit_apdu(connection, apdu):
"""Send APDU and handle T=0 GET RESPONSE automatically."""
resp, sw1, sw2 = connection.transmit(apdu)
if sw1 == 0x61:
# GET RESPONSE with Le from SW2
get_resp = [0x00, 0xC0, 0x00, 0x00, sw2]
resp, sw1, sw2 = connection.transmit(get_resp)
elif sw1 == 0x6C:
# Wrong Le — retry with correct Le from SW2
apdu[-1] = sw2
resp, sw1, sw2 = connection.transmit(apdu)
return resp, sw1, sw2
# Usage:
response, sw1, sw2 = transmit_apdu(connection, SELECT_MF)
7. Monitor Card Insertion and Removal
from smartcard.CardMonitoring import CardMonitor, CardObserver
import time
class CardWatcher(CardObserver):
def update(self, observable, actions):
(added, removed) = actions
for card in added:
print(f"Card INSERTED: {card.atr}")
for card in removed:
print("Card REMOVED")
monitor = CardMonitor()
watcher = CardWatcher()
monitor.addObserver(watcher)
print("Waiting for card events... (Ctrl+C to stop)")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
monitor.deleteObserver(watcher)
print("Done")
8. Send Commands to a Specific Protocol (T=0 or T=1)
# Force T=0 conn_t0 = r[0].createConnection() conn_t0.connect(protocol="T=0") # Force T=1 conn_t1 = r[0].createConnection() conn_t1.connect(protocol="T=1") # Auto-negotiate (recommended) conn_auto = r[0].createConnection() conn_auto.connect() # Negotiates highest available protocol
Common Error Codes and Fixes
| Error | Meaning | Fix |
|---|---|---|
No readers found | Reader not detected | Check USB, reinstall driver, restart pcscd (Linux) |
Card not present | No card on reader | Insert card before connecting |
Connection refused | Another app locked the reader | Close other smart card apps, restart pcscd |
SmartcardException: transmit() | APDU command failed | Check CLA/INS/P1/P2 with APDU Builder |
Try Our Tools While Coding
APDU Command Builder — Build commands visually and copy hex to your Python code | APDU Response Debugger — Debug SW1 SW2 | ATR Decoder — Parse ATR hex | PC/SC & CCID Programming Guide — Deep dive into reader architecture