I recently ran into a frustrating error while working on a small blockchain app in Python with Flask. Everything looked fine until I tried to call my /mine
endpoint. Suddenly, Flask threw this at me:
TypeError: 'dict' object is not callable
At first, I wasn’t sure where I’d gone wrong. After digging in, I realized it was a common Python gotcha. I’ll walk you through:
- The original buggy snippet
- Why this error happens and the simple fix
- A polished, working blockchain app in Flask (with extra endpoints so you can practice and extend)
The Original Snippet
Here’s the piece of code that caused me headaches:
class Blockchain(object):
@property
def end_block(self):
last_block = self.chain[-1]
return last_block
@app.route('/mine', methods=['GET'])
def mine():
last_block = blockchain.end_block() # <-- boom
last_hash = blockchain.hash(last_block)
proof = blockchain.proof_of_work(last_hash)
...
At first glance, it looks fine grab the last block, hash it, and mine the next one. But Python didn’t agree.
Why the Error Happen
The culprit is @property
.
When I decorate a method with @property
, it behaves like a data attribute—not a function. That means:
blockchain.end_block
returns a dictionary (my last block).- But I wrote
blockchain.end_block()
, which tries to call that dictionary as if it were a function.
Python then yells:TypeError: 'dict' object is not callable
.
The Fix (Two Options)
I had two choices:
Keep it as a property (no parentheses).
last_block = blockchain.end_block
Make it a method (more intuitive in this case).
def end_block(self):
return self.chain[-1]
# Then call it with:
last_block = blockchain.end_block()
I prefer Option B because “get the last block” feels like an action. Either way, consistency is key pick one and use it everywhere.
A Polished Blockchain Flask App
Once I fixed that, I decided to flesh out the project into a working blockchain API with some extra practice endpoints. Here’s the full code:
from __future__ import annotations
from hashlib import sha256
import json
from time import time
from uuid import uuid4
from typing import Any, Dict, List, Optional
from flask import Flask, jsonify, request
def sha256_hex(data: str) -> str:
return sha256(data.encode("utf-8")).hexdigest()
class Blockchain:
def __init__(self, difficulty: int = 4):
self.chain: List[Dict[str, Any]] = []
self.current_transactions: List[Dict[str, Any]] = []
self.difficulty = difficulty
# Create the genesis block
self.add_block(proof=100, previous_hash="1")
def add_block(self, proof: int, previous_hash: Optional[str] = None) -> Dict[str, Any]:
block = {
"index": len(self.chain) + 1,
"timestamp": time(),
"transactions": self.current_transactions,
"proof": proof,
"previous_hash": previous_hash or self.hash(self.chain[-1]),
}
self.current_transactions = []
self.chain.append(block)
return block
def new_transaction(self, sender: str, recipient: str, amount: int | float) -> int:
self.current_transactions.append({
"sender": sender,
"recipient": recipient,
"amount": amount,
})
return self.last_block()["index"] + 1
@staticmethod
def hash(block: Dict[str, Any]) -> str:
block_string = json.dumps(block, sort_keys=True, separators=(",", ":"))
return sha256_hex(block_string)
def last_block(self) -> Dict[str, Any]:
return self.chain[-1]
def proof_of_work(self, last_proof: int, last_hash: str) -> int:
proof = 0
target = "0" * self.difficulty
while True:
guess = f"{last_proof}{last_hash}{proof}"
guess_hash = sha256_hex(guess)
if guess_hash.startswith(target):
return proof
proof += 1
def valid_chain(self, chain: List[Dict[str, Any]]) -> bool:
if not chain:
return False
for i in range(1, len(chain)):
prev = chain[i-1]
curr = chain[i]
if curr["previous_hash"] != self.hash(prev):
return False
if not self._valid_proof(prev["proof"], self.hash(prev), curr["proof"]):
return False
return True
def _valid_proof(self, last_proof: int, last_hash: str, proof: int) -> bool:
target = "0" * self.difficulty
guess = f"{last_proof}{last_hash}{proof}"
return sha256_hex(guess).startswith(target)
# --- Flask app setup ---
app = Flask(__name__)
node_id = str(uuid4()).replace("-", "")
blockchain = Blockchain(difficulty=4)
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"}), 200
@app.route("/difficulty", methods=["GET"])
def difficulty():
return jsonify({"difficulty": blockchain.difficulty}), 200
@app.route("/pending", methods=["GET"])
def pending():
return jsonify({
"pending_transactions": blockchain.current_transactions,
"count": len(blockchain.current_transactions)
}), 200
@app.route("/transactions/new", methods=["POST"])
def transactions_new():
data = request.get_json(force=True, silent=True) or {}
required = {"sender", "recipient", "amount"}
if not required.issubset(data):
return jsonify({"error": "Missing fields", "required": sorted(required)}), 400
try:
amount = float(data["amount"])
except (TypeError, ValueError):
return jsonify({"error": "amount must be a number"}), 400
index = blockchain.new_transaction(
sender=str(data["sender"]),
recipient=str(data["recipient"]),
amount=amount,
)
return jsonify({"message": f"Transaction will be included in block {index}"}), 201
@app.route("/mine", methods=["GET"])
def mine():
last_block = blockchain.last_block() # <-- fixed
last_proof = last_block["proof"]
last_hash = blockchain.hash(last_block)
proof = blockchain.proof_of_work(last_proof, last_hash)
blockchain.new_transaction(sender="0", recipient=node_id, amount=1)
block = blockchain.add_block(proof=proof, previous_hash=last_hash)
response = {
"message": "New block forged",
"index": block["index"],
"transactions": block["transactions"],
"proof": block["proof"],
"previous_hash": block["previous_hash"],
"hash": blockchain.hash(block),
"miner": node_id,
}
return jsonify(response), 200
@app.route("/chain", methods=["GET"])
def full_chain():
return jsonify({"length": len(blockchain.chain), "chain": blockchain.chain}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
Quick Practice With curl
Add a transaction:
curl -X POST http://127.0.0.1:5000/transactions/new \
-H "Content-Type: application/json" \
-d '{"sender":"alice","recipient":"bob","amount":3.5}'
Mine a block:
curl http://127.0.0.1:5000/mine
See pending transactions:
curl http://127.0.0.1:5000/pending
Get the full chain:
curl http://127.0.0.1:5000/chain
Final Thought
This error taught me something simple but important: in Python, the difference between a property and a method can cause subtle bugs. Whenever I see “object not callable”, I now double-check for stray parentheses after things like lists, dicts, or properties. On top of fixing the bug, expanding the blockchain project with extra endpoints gave me a sandbox to practice Flask and blockchain basics at the same time.