Last Updated: May 22, 2026
Inheritance lets one class take on the fields, properties, and methods of another, so you don't rewrite the same code every time a new kind of thing shows up. In C#, a class can extend exactly one other class with the : BaseClass syntax, and the new class inherits every accessible member of the original. This chapter covers what inheritance actually is, why you'd use it, the syntax for declaring a derived class, which members carry over and which don't, the role of access modifiers (especially protected), the single-inheritance rule, and when you should reach for composition instead.
Consider an online store that sells two kinds of products: books and electronics. Both have a name, a price, and a stock count. Both should be able to print a summary, calculate a discounted price, and check if they're in stock. Books also have an author and a page count. Electronics have a warranty period and a power rating. Written without inheritance, the two classes look like this:
Three fields, three methods, all identical, repeated in two classes. Add a Toy class next and you'll copy them a third time. Decide tomorrow that PrintSummary should include a currency symbol from a config setting, and you'll edit it everywhere. The problem isn't that the code is wrong, it's that the duplication invites bugs. One day you'll forget to update one of the copies.
Inheritance removes the duplication. You declare a base class called Product that holds the shared data and behavior, then declare Book and Electronics as classes that inherit from Product:
Book and Electronics each declared only their own extra fields. The shared Name, Price, Stock, PrintSummary, GetDiscountedPrice, and IsInStock carry over through inheritance, defined once in Product and reused by both. Adding a Toy class next is now a three-line declaration, not a fifty-line copy.
That captures the two reasons inheritance exists:
Product can work with a Book, an Electronics, or anything else that inherits from Product. The full power of that idea is polymorphism.There's a third benefit, less obvious at this stage: inheritance creates a vocabulary. Saying "everything that's a Product" is shorter and clearer than "everything that has a name, a price, and a stock." The base class gives a name to the abstraction.
: BaseClass SyntaxTo declare that one class inherits from another, put a colon and the base class name after the derived class name:
In this declaration, Product is the base class (sometimes called the parent class or superclass), and Book is the derived class (also called the child class or subclass). Reading the line aloud, "class Book derives from Product" or "class Book is a Product" both work.
A small picture of the relationship:
The arrows point from the base class to each derived class to show the inheritance chain. Every member declared on Product exists on Book, Electronics, and Toy, plus each derived class adds its own extras. The convention in many UML-style diagrams is to draw the arrow the other way (from derived to base), but the direction of the arrow doesn't matter for understanding. What matters is which class owns which member.
The derived class can be used wherever the syntax expects either its own type or its base type. Setting a field declared on the base class works through any instance of the derived class:
From the outside, a Book object looks like a single bag of fields: Name, Price, Author. The compiler knows which class declared each one, but at the usage site you just dot into them.
You can also assign a Book reference to a Product variable, because every Book is a Product:
The variable p is typed as Product, so the compiler only lets you reach members declared on Product or its ancestors. The underlying object is still a Book, just viewed through the narrower lens of the Product type. This is called upcasting, and it's the mechanism that makes polymorphism work. The Polymorphism section returns to it.
When you write class Book : Product, not every line inside Product is copied into Book blindly. The rule is precise: a derived class inherits all members of its base class except constructors, finalizers, and static constructors. Whether the derived class can actually see and use each inherited member depends on the member's access modifier.
Here's a table summarizing what carries over:
| Member declared on base | Inherited by derived? | Accessible inside derived class? |
|---|---|---|
public field, property, method | Yes | Yes |
protected field, property, method | Yes | Yes |
internal field, property, method | Yes | Yes (within the same assembly) |
private field, property, method | Yes (technically) | No |
| Constructor | No | N/A |
| Finalizer | No | N/A |
| Static constructor | No | N/A |
static member | Yes (callable through the derived class name) | Yes |
The "technically inherited but not accessible" row is the easy one to misread. A private field declared on the base class is part of every derived instance's data, but the derived class's code can't read or write it directly. It's there in memory; you just don't have the key. This matters because methods inherited from the base class can still touch those private fields (the base's own code has the access), so the inheritance is real even though the visibility isn't.
A short program makes the difference concrete:
The class Product declares three different visibility levels. Name is public, so anyone can read or set it. cost is protected, so Book can use it inside its own methods, but outside code can't. sku is private, so only Product's own code touches it directly; Book can only reach it through the public methods SetSku and GetSku. Each access modifier draws a different boundary, and inheritance respects each one.
The fact that constructors aren't inherited is worth pausing on. Writing class Book : Product { } does not automatically give Book a constructor that takes the same parameters as Product's constructor. The derived class always declares its own constructors, and they call into the base class's constructor as part of their setup. For this lesson, we're sticking with the implicit parameterless constructor that C# generates when you don't write one.
Inheritance doesn't make objects bigger than the union of all inherited fields. A Book object on the heap holds slots for Name, Price, Stock (from Product), plus Author and Pages (from Book). There's no per-class overhead beyond what each member needs. The virtual method table (covered with virtual/override) adds a small constant cost per object regardless of depth.
You met public and private in the _Access Modifiers_ lesson. Inheritance is where the third common modifier, protected, becomes useful. A short refresher of all four, focused on what they mean once a base/derived relationship exists:
| Modifier | Same class | Same assembly | Derived class | Anywhere |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | No | Yes | No |
internal | Yes | Yes | No (unless also in the same assembly) | No |
private | Yes | No | No | No |
protected internal | Yes | Yes | Yes | No |
private protected | Yes | No (same assembly only) | Yes (same assembly only) | No |
The two combined modifiers at the bottom (protected internal and private protected) are less common and exist to fine-tune access between assemblies. For the next several chapters, the three to focus on are public, private, and protected.
The rule of thumb most C# developers follow:
protected.protected shows its value when you have data that the base class wants to manage but derived classes need to read or update. Take a base Product that tracks its own stock and offers a hook for derived classes to reduce stock when something product-specific happens (a book is reserved for a library, an electronic item gets returned for repair):
Outside callers (the Main method here) can't change stock directly or call ReduceStock. The base class controls how stock moves through SetStock and the protected ReduceStock helper. Inside Book, the method ReserveForLibrary is allowed to call ReduceStock because Book inherits from Product. That's the slot protected fills: a member visible to the inheritance hierarchy but invisible to everyone else.
A common beginner mistake is to make everything public so the code "just works." That choice voids the contract a class is supposed to enforce. If stock were public, anyone could set it to -1000 and break invariants like "we never ship more than we have." Picking the narrowest modifier that still lets the necessary code compile is the discipline that keeps a codebase from rotting.
C# allows a class to inherit from exactly one base class. You can't write class Hybrid : Foo, Bar and expect to pick up members from both. Try it, and the compiler refuses:
This is called the single inheritance rule, and it's a deliberate design choice in C# (and many modern languages). Languages that allow multiple inheritance of classes have to answer awkward questions like "if both base classes declare a method with the same name, which one does the derived class inherit?" or "if a derived class inherits from two classes that both inherit from a shared ancestor, do you get one copy or two of the ancestor's data?" These problems have solutions, but the solutions add complexity that most code doesn't need.
The escape hatch is interfaces, which a class can implement in unlimited numbers. An interface describes what a class can do (a list of methods and properties) without providing any implementation. So you can write class Book : Product, IPrintable, ISearchable to inherit from one class and implement two interfaces. Interfaces are the topic of the Abstraction section. For now, just remember that "one base class, many interfaces" is the C# way.
Inheritance can chain. A class derived from a base can itself be a base for another class:
TextBook inherits everything from Book, which itself inherits from Product. So a TextBook ends up with four fields: Name, Price, Author, Subject. The chain can be as deep as you want, though most well-designed C# code keeps inheritance shallow (one or two levels). Deep hierarchies become hard to follow, because to understand what a leaf class can do, you have to read every ancestor up to the root.
The chain visualized:
The top node, object, deserves its own subsection.
System.ObjectWhen you declare a class with no : BaseClass clause, C# silently inserts : System.Object for you. So this:
is exactly equivalent to:
Every class in C# ultimately inherits from System.Object, which is also written with its keyword alias object. There is no class in the .NET type system that doesn't trace back to object at the root. Even int and decimal, which are value types, share this root through a different mechanism (boxing).
object declares a handful of methods that every class therefore inherits:
Method on object | What it does |
|---|---|
ToString() | Returns a string that represents the object. Default is the type name. |
Equals(object?) | Compares two references (or values, after overriding). |
GetHashCode() | Returns an integer hash, used by Dictionary and HashSet. |
GetType() | Returns a Type object describing the runtime type. |
A tiny demonstration:
Output (your hash code will differ):
ToString() and GetType() both produce the type name (Product) because Product didn't override ToString and the default just returns the class name. GetHashCode() returns a number derived from the object's identity in memory, which is why it changes between runs.
The full breakdown of these methods, plus the rules for overriding them correctly, lives in this section's lesson 7 ("Object Class") and lesson 9 ("Equals & GetHashCode"). The point to take away here is that you never have to write : object yourself. The compiler does it for any class that doesn't declare a different base.
One small consequence is that the chain in the previous diagram is more precise once we add the implicit root:
Whether you write the : object or leave it off, the runtime treats them the same. Most C# code leaves it off.
To pull everything together, here's a slightly bigger E-Commerce example that uses inheritance the way real C# code does. The base class Product knows the basics of any item in the store. Book and Electronics derive from it and add their own details. A small Main builds a cart-like list and uses members from both levels.
The List<Product> holds both a Book and an Electronics, which is the upcasting in action. Iterating that list calls PrintSummary on each item, and each derived class uses the inherited implementation from Product without writing anything itself. The protected field stock is updated through the public Restock method, while readers can see the value through the Stock property (a getter-only property). Each derived class adds exactly what's unique to it.
If you needed to add a Toy class next, you'd write:
Four lines. Every Toy instance now has a name, price, stock, the ability to restock, and the ability to print a summary. Nothing else has to change. That payoff is what makes inheritance worth the rules around it.
Inheritance is one way to build complex things from simpler ones. The other is composition, where a class holds an instance of another class as a field rather than inheriting from it. They solve overlapping problems with very different trade-offs, and picking the right one for a given relationship matters more than any specific syntax.
The phrase to repeat in your head is "is-a" vs "has-a". A Book is a Product, so inheritance fits. A Book has an Author, but a Book is not an Author, so the relationship should be a field, not inheritance:
The same Book uses both. It inherits from Product because every book really is a product on the store's shelves. It holds an Author because every book really has an author, but a book is not an author. Mixing the two relationships up (making Book inherit from Author to "reuse" the name fields, for example) produces a model that's wrong, and the wrongness shows up in the code that consumes it.
A small rule of thumb most experienced C# developers follow: prefer composition unless inheritance is clearly the right model. Composition is more flexible, because you can swap or rearrange components without changing the type hierarchy. Inheritance commits you to the relationship at compile time, and changing it later means changing every consumer of the type. When in doubt, hold an instance rather than inherit from it. You can always introduce inheritance later if the "is-a" relationship really does emerge, but ripping it out once it's in is painful.
A short table to keep them straight:
| Question | Inheritance | Composition |
|---|---|---|
| Relationship | "is-a" | "has-a" |
| Coupling | Tight (derived knows the base) | Loose (holder uses the held object's public API) |
| Flexibility | Low (single base class, fixed at compile time) | High (swap components freely) |
| Polymorphism | Yes (built-in) | Yes (through interfaces on the field's type) |
| Reuse pattern | Inherit existing fields and methods | Delegate to fields |
We'll come back to this trade-off in the design patterns section, where several patterns (Decorator, Strategy) deliberately favor composition over inheritance. For now, the takeaway is: inheritance is a tool, not a default. Use it when one class genuinely is a kind of another, and stick with composition the rest of the time.
The full set of ideas from this chapter, condensed:
: BaseClass after its name.public and protected are visible, internal is visible within the same assembly, private is not.protected is the access modifier designed for inheritance: visible to the declaring class and its descendants, hidden from everything else.System.Object (alias object), which is the root of the type system.The pieces this chapter intentionally didn't cover:
base keyword. Covered in _base Keyword_.virtual and override. Covered in _Method Overriding (virtual/override)_.new keyword. Covered in _new Keyword (Method Hiding)_.sealed. Covered in _Sealed Classes & Methods_.System.Object methods (ToString, Equals, GetHashCode, GetType) in detail. Covered in _Object Class_.is and as. Covered in _is & as Operators_.Equals and GetHashCode correctly. Covered in _Equals() & GetHashCode()_.Inheritance covered the "what" and "why" here. We've inherited members, but what if we want to call or extend a base class method from the derived class? That's what the base keyword is for.