How to Fix the “Dict Object Not Callable” Error in a Python Flask Blockchain App

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:

  1. The original buggy snippet
  2. Why this error happens and the simple fix
  3. 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.

Related blog posts