Concepts / Python Classes and Methods

Python Classes and Methods

A custom Dataset class in PyTorch is built by subclassing the Dataset parent class and implementing __init__, __getitem__, and __len__.

  • Programming
Interactive lab

Try it: Classes, Instances and Attributes

How a class creates instances with __init__ and self, why an attribute read checks the instance's __dict__ before the class, why assigning through an instance never changes the class, how a class-level list is shared by every instance, and why obj.method(x) is Class.method(obj, x).

How it works

  1. Dog(...) makes a new, empty instance and calls Dog.__init__(new_object, ...); self.name = ... stores into that object's __dict__.
  2. Reading obj.attr looks in obj.__dict__ first, then in the class.
  3. Assigning obj.attr = value always writes obj.__dict__, so it shadows a class attribute for that one object only.
  4. Mutating a class-level list (self.tricks.append) changes the one list every instance shares.
  5. obj.method is found on the class and bound to obj, so obj.method(x) runs Class.method(obj, x).

Default run (30 steps): Ready: class Dog and the statements you picked. Watch where each attribute is found and where each assignment writes. … Finished. Printed: ['sit'] / Wolf / Canis familiaris / Fido is 5.

Simplified: The lab's own small Python interpreter runs a fixed, hand-written script (you choose the variant and the values, never the code); nothing is executed as real Python. Every script was run in CPython 3.12 with the same inputs and the lab reproduces its output and error messages. Object numbers are illustrative, not real id() values.

Educational simulation

Loading the simulation…

When the Dataset Returns the Wrong Thing

Suppose you build a dataset object, ask it for an item, and get back something you did not expect. Perhaps the wrong feature is paired with the wrong label, or the length does not match your data. Where should you look? This article approaches custom dataset classes from that debugging question. Rather than starting with a list of rules, it starts with behavior you can observe when a custom dataset runs, and then works backward to the code that produces it.

A custom dataset class in PyTorch begins by subclassing PyTorch's Dataset parent class. The subclass inherits from Dataset, and then supplies the behavior that makes the data specific to your problem. Every custom dataset you write is built from three methods: __init__, __getitem__, and __len__. Each one owns a different responsibility, and a wrong result almost always points to one of them rather than to the class as a whole.

What do you think happens?

Imagine two lists, one holding features and one holding labels, aligned so that position 2 in each holds the features and label for the same example. If you ask the dataset for the item at index 2, what should come back, and what should the dataset's length report? Write down your forecast before reading on.

Reveal answer
Answer:

The item at index 2 should be the pair made of the features and the label stored at position 2, and the length should equal the number of aligned entries in the lists.

Keep this forecast. The point of tracing the class is to compare it against what the code actually does, and to notice the exact moment your mental model and the code disagree. That moment is usually where the bug lives.

Tracing One Request Through the Class

A helpful way to read a custom dataset is to follow a single request from start to finish. The request begins with an index, moves into __getitem__, reads the stored arrays at that position, and returns a pair. Trace this path once and you will know which attributes were touched and which line produced the final value. The source example works with tensors of numerical features. The sketch below uses plain Python lists instead, so the structure stays easy to see. The three methods play the same roles as they would in the tensor version.

python
Output (expected)
4
('loved it', 1)
Notes on the code
  • __init__ runs once, when ReviewDataset(...) is called, and stores both lists on self
  • __getitem__ receives one index and returns one (text, label) pair
  • __len__ reports how many labels are stored
  • len(reviews) calls __len__, and reviews[2] calls __getitem__ with index 2
texts[0]great filmlabels[0]1texts[1]dull plotlabels[1]0index 2texts[2]loved itlabels[2]1texts[3]too longlabels[3]0selectsselects
When reviews[2] runs, the index 2 picks the same position in both the texts list and the labels list, and __getitem__ returns those two values together as one pair.
Worked example

Tracing reviews[2] Line by Line

What does reviews[2] return for the ReviewDataset built in the sketch above?

  1. 1
    Call the index operator

    Python sees reviews[2] and calls the object's __getitem__ method, passing 2 as the index. The data loader would normally supply this index; here we supply it ourselves.

  2. 2
    Read the texts array

    Inside __getitem__, self.texts[index] becomes self.texts[2], which is the string 'loved it'. The attribute self.texts was stored earlier by __init__.

  3. 3
    Read the labels array

    self.labels[index] becomes self.labels[2], which is the integer 1. The label sits at the same position as its feature, so the two stay paired.

  4. 4
    Return the pair

    The return statement packages both values into a tuple, giving ('loved it', 1).

Result

('loved it', 1). The features and label at position 2 come back together, which matches the forecast from the start of the article.

Who Owns What Among the Three Methods

The three methods divide the work. One stores the data, one returns a single sample, and one reports the size. Separating them this way means you can ask a precise question about any wrong result: is the stored data wrong, is the sample selected wrongly, or is the count wrong? The diagram below shows how the three methods depend on the same stored attributes.

ReviewDataset__init__stores dataself.texts andself.labelsstored arrays__getitem__returns one sampleone (text, label)pair__len__reports sizenumber of labelsdefinesdefinesdefinessaves to selfreads at indexreturnsmeasuresreturns
The class definition contains three methods. __init__ saves the arrays, __getitem__ reads them at one index and returns a sample, and __len__ measures them. The two later methods depend on what the first one stored.

Following a Call Sequence

The two operations you perform on a dataset, indexing and measuring, trigger different methods in a fixed pattern. Writing reviews[2] runs __getitem__ with index 2, and writing len(reviews) runs __len__. Neither call touches __init__, because __init__ ran earlier, when the object was built. The sequence below follows both calls through the object and its stored arrays.

callerReviewDatasetself.textsself.labelsreviews[2] calls __getitem__(2)reads index 2reads index 2returns ('loved it', 1)len(reviews) calls __len__()measures stored labelsreturns 4
The caller sends a request to the dataset object. For indexing, the dataset reads both stored arrays at index 2 and returns the pair. For measuring, the dataset reads the label array and returns its count.

Mistakes That Make Output Diverge

When a returned sample is wrong, work along the path from __init__ to __getitem__ to the return value. Check the stored attributes first, then the indexing inside __getitem__, then the count reported by __len__. The first step where the output stops matching your forecast is where the divergence starts.

  • Common mistake

    Assuming __getitem__ chooses which item to return

    What it looks like

    Calling reviews[2] and expecting the class to pick a random or convenient sample.

    Why it fails

    The index is supplied by the caller, and in training the data loader supplies it. __getitem__ returns exactly the item for the index it receives.

    Do this instead

    Treat the index as the input. If the wrong item comes back for index 2, look at how the stored arrays are read at that index, not at how the index was chosen.

  • Common mistake

    Keeping data in a local variable inside __init__

    What it looks like
    def __init__(self, texts, labels):
        stored = texts
    Why it fails

    A local variable exists only while __init__ runs. __getitem__ cannot read it, so it fails when it looks for self.texts.

    Do this instead

    Attach the data to self, as in self.texts = texts, so the other methods can read it later.

  • Common mistake

    Measuring the wrong stored array in __len__

    What it looks like

    Returning len(self.texts) when the labels list was supposed to define the size.

    Why it fails

    The reported length then describes a different array from the one the samples come from, so the count can disagree with the data you believe you loaded.

    Do this instead

    Make __len__ measure the array that defines the samples, as the source example does with the row count of the label array.

  • Common mistake

    Building the feature list and the label list in different orders, so that position 2 no longer pairs the same example in both.

Practice: Trace a Second Dataset

MEDIUM

Using the ReviewDataset class from earlier, predict the output of these two lines before running them: reviews2 = ReviewDataset(['fun ride', 'boring', 'a classic'], [1, 0, 1]), then print(len(reviews2)) and print(reviews2[1]). Write your prediction for both printed lines, then identify which method produced each one.

Hints
  • len(reviews2) calls __len__, which measures the stored label list.
  • reviews2[1] calls __getitem__ with index 1, so look at position 1 in both lists.
  • Remember that the labels list is 1, 0, 1, so position 1 holds 0.
MEDIUM

A teammate's dataset returns ('boring', 1) for index 1 when the label list is [1, 0, 1]. Without running any code, name the method where the divergence most likely starts, and explain which stored value you would check first.

Hints
  • Ask whether the feature side or the label side is wrong, using the list contents.
  • Think about which method reads the stored arrays at a given index.

Key Takeaways

  • A custom Dataset class is built by subclassing PyTorch's Dataset parent class and implementing __init__, __getitem__, and __len__.
  • __init__ runs once to store the data on self, so the other methods can read it later.
  • __getitem__ returns exactly one item for the index it receives, and the data loader supplies that index.
  • __len__ reports the number of items, reading the stored label array in the source example.
  • To locate a wrong output, follow the path from the stored attributes to the index lookup to the return value, and stop at the first step where the result differs from your forecast.

Related concepts

More concepts starting with P