Build a Cross-Language Generic Pipeline
Build matching Java and Python pipelines to explore generic type safety.
Introduction
30 Second Summary
A number can become text as it moves through a program. Trouble starts when the next piece of code still assumes it received a number.
In this project, you will build the same data pipeline with generics in Java and Python. Deliberate type mismatches reveal when each language catches the mistake.
What You'll Build
Run the matching pipelines to watch 21 become Total: $42 while both programs show that different type arguments still share one runtime class.
By the end of this project, you'll have:
- Run an unsafe Java packet that prints Mapped value: Total: $42 before an invalid cast triggers ClassCastException.
- Compare Java compiler feedback with Python static checking. You'll see Java reject a mismatch while Python runs a bad annotation that mypy flags.
- Run the corrected pipelines to print Mapped value: Total: $42. Their type erasure checks report the same runtime class in both languages.
- Secret Mission: Add a combine operation that turns two typed packets into a third packet with a new result type.
Are there any prerequisites?
This project assumes you are comfortable reading Java and Python in Visual Studio Code. Step 1 verifies Python 3.10 or later plus a Java Development Kit with javac before setting up mypy 2.3.1.
Before We Start
You are about to build matching Java and Python DataPacket pipelines that transform the integer 21 into the string Total: $42. The comparison follows type information through compiler checks, static analysis, runtime failures, and type erasure.
Prepare the Cross-Language Lab
A fair comparison between Java and Python depends on having every tool available in one reproducible workspace. A missing compiler or incompatible checker could blur the source of a failure.
You will prepare Visual Studio Code for both language demos. You will also create a local virtual environment containing mypy 2.3.1.
In this step, get ready to:
- Confirm that Python 3.10 or later is available.
- Build the generics-lab workspace structure.
- Install mypy 2.3.1 inside a disposable virtual environment.
Check the installed runtimes
The Python pipeline needs Python 3.10 or later because that is the minimum supported by this mypy release. The Java pipeline needs the compiler supplied by a Java Development Kit.
- Press Cmd+Space on macOS to open the search bar.
- Type Visual Studio Code into the search bar.
- Press Enter to launch Visual Studio Code.
- Click Terminal in the top menu bar.
- Click New Terminal.
- Check your Python version by running this command:
python3 --version
What does this check show?
The command prints the Python interpreter version attached to python3. This confirms whether it meets mypy's minimum of Python 3.10.
✔️ Python meets the requirement
Your Python runtime is ready when the version is 3.10 or later. You can continue to the Java compiler check.
ⓧ Python is too old
A Python version older than 3.10 cannot run mypy 2.3.1.
- Use the current macOS installer from the official Python guide.
- Return to the VS Code terminal.
- Repeat the compatibility check by running this command:
python3 --version
What should you see?
The updated command should print Python 3.10 or later. That version can create the environment used in this project.
ⓧ python3 is unavailable
Your terminal cannot find a Python interpreter through the python3 command.
- Install Python using the official macOS guide.
- Return to the VS Code terminal.
- Repeat the compatibility check by running this command:
python3 --version
What should you see?
The command should now print Python 3.10 or later. This confirms that the terminal can find the installed interpreter.
- Check the Java compiler by running this command:
javac -version
What does this check show?
The javac command prints compiler version information. A version line confirms that the Java compiler is available to this terminal.
You should see a compiler version line. This proves the Java source files in later steps can be compiled locally.
Is javac unavailable?
- Install a JDK using Oracle's macOS JDK installation guide.
- Return to the VS Code terminal.
- Repeat the Java compiler check above.
If the compiler still cannot be found, help me troubleshoot my JDK installation.
Create the workspace and dependency file
A shared workspace keeps the Java source separate from the Python source. The dependency file records the exact mypy release needed to reproduce the static checks.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The cd command changes the terminal's current folder. The ~ shortcut represents your macOS home folder.
- Build the workspace structure by running these commands:
mkdir -p generics-lab/java generics-lab/python
cd generics-lab
What do these commands create?
- The generics-lab folder contains the complete comparison project.
- The java folder holds the Java demonstrations.
- The python folder holds the Python demonstration.
- List the new workspace contents by running this command:
ls
What should you see?
The terminal should list java plus python. This confirms both language folders exist inside generics-lab.
- Click File in the Visual Studio Code menu bar.
- Click Open Folder.
The folder picker is now ready for the workspace location.
- Select the generics-lab folder on your Desktop.
- Click Open.
You should see java plus python in the Explorer sidebar.
- Click the New File icon in the Explorer sidebar.
A filename field appears beneath the generics-lab folder.
- Type requirements.txt into the filename field.
- Press Enter.
The blank requirements.txt file opens in the editor.
- Pin the project dependency by pasting this line into requirements.txt:
mypy==2.3.1
Why pin the version?
The double equals sign requests the exact mypy release used throughout the project. Every static-checking result now comes from the same checker version.
- Press Cmd+S on macOS or Ctrl+S on Windows to save requirements.txt.
You should see requirements.txt beside the java folder plus the python folder in Explorer.
Is the file in the wrong folder?
Check that requirements.txt appears directly beneath generics-lab. It should sit beside both language folders.
If the folder layout differs, help me fix my generics-lab workspace structure.
✔️ Awesome, I've got everything!
Great. Your dependency file is saved with the exact mypy pin.
ⓧ I'd like to double check the full code
- Compare your saved requirements.txt with this complete file:
mypy==2.3.1
Create the virtual environment and install mypy
The project-local environment keeps mypy separate from packages installed elsewhere on your Mac. Its executables can run directly from .venv/bin without activation.
- Create the disposable environment by running this command from generics-lab:
python3 -m venv .venv
What does this command create?
Python creates an isolated .venv directory inside generics-lab. The directory includes its own Python executable plus package installer.
You should see .venv appear in the Explorer sidebar. This confirms the local environment exists.
Did the environment fail to appear?
Confirm that the terminal prompt points to generics-lab. A different current folder places .venv somewhere else.
If environment creation reports an error, help me debug python3 -m venv on macOS.
- Install the pinned dependency into .venv by running this command:
.venv/bin/python -m pip install -r requirements.txt
How does this install stay local?
The command invokes the Python executable inside .venv. Pip reads requirements.txt to install the pinned mypy release into that environment.
You should see package installation output finish without an error. This confirms that the local environment contains the checker.
Did the installation fail?
Confirm that requirements.txt contains exactly mypy==2.3.1. Also confirm that your terminal is still inside generics-lab.
If pip reports another problem, help me debug the local mypy installation.
Before you run the final check, which three version lines do you expect the terminal to print?
- Verify the complete lab toolchain by running these commands:
python3 --version
javac -version
.venv/bin/mypy --version
What should you see?
- The first line should report Python 3.10 or later.
- The second line should report a javac compiler version.
- The final line should include mypy 2.3.1.
You have cleared the environment risk: all three type-system experiments now share one reproducible workspace.
Your cross-language lab is ready. Next, you will build an untyped Java packet that lets a bad cast reach runtime.
Run the Untyped Java Failure
Your Visual Studio Code workspace now has every compiler and checker needed for the comparison. You can start by seeing how Java behaves after specific type information disappears.
An Object-based packet hides the relationship between its input and output. This step deliberately lets an invalid cast survive compilation before it fails at runtime.
In this step, get ready to:
- Build a Java packet that stores and transforms values as Object.
- Compile the source while its specific output type remains hidden.
- Run the program to observe a successful transformation followed by a runtime cast failure.
Build the untyped packet
An Object reference can hold an integer or a string. The packet therefore has no type parameter that records which kind of value it currently contains.
- In the java directory shown in Visual Studio Code's file sidebar, create UnsafePacketDemo.java with the new-file icon.
You'll see the empty UnsafePacketDemo.java file inside the java directory.
- Define the untyped transformation contract and packet by pasting this code into UnsafePacketDemo.java:
interface UntypedTransformer {
Object apply(Object input);
}
final class UntypedPacket {
private final Object value;
UntypedPacket(Object value) {
this.value = value;
}
Object value() {
return value;
}
UntypedPacket map(UntypedTransformer transformer) {
return new UntypedPacket(transformer.apply(value));
}
}
What does this code do?
- UntypedTransformer defines an apply() operation whose input has the broad Object type. Its output uses the same broad type.
- UntypedPacket stores its current value in an Object field. The field can therefore hold different kinds of values at different points.
- map() passes the stored value to a transformer. It wraps the returned Object in another packet.
- Save UnsafePacketDemo.java.
- Compile the two type definitions by running this command in the terminal from earlier:
javac java/UnsafePacketDemo.java
The command asks javac to compile the source into class files. You'll return to the terminal prompt without a compiler error.
Seeing a compiler error?
Confirm that UnsafePacketDemo.java sits inside the java directory. Compare each brace and method signature with the code above.
Help me debug the Java compiler error.
Add the runnable demonstration
The packet needs a main() method to demonstrate how its stored value changes. The method also makes an explicit request for an integer after the mapping completes.
- Add the runnable demonstration below the final closing brace in UnsafePacketDemo.java by pasting this code:
public class UnsafePacketDemo {
public static void main(String[] args) {
UntypedPacket amount = new UntypedPacket(21);
UntypedPacket label = amount.map(
value -> "Total: $" + (((Integer) value) * 2)
);
System.out.println("Mapped value: " + label.value());
Integer impossible = (Integer) label.value();
System.out.println("This line is never reached: " + impossible);
}
}
How does the demonstration work?
- amount begins as an UntypedPacket containing 21.
- The transformer casts its input to Integer. It doubles the number before formatting a string.
- label receives the transformed packet. Its declaration still records only UntypedPacket.
- impossible explicitly asks Java to treat label.value() as an Integer.
- Save UnsafePacketDemo.java.
- Compile the completed source by running this command:
javac java/UnsafePacketDemo.java
The command compiles the completed program. You'll return to the prompt without an error because the source contains an explicit cast from Object.
Good work. Java has accepted the complete untyped packet demonstration.
Did the completed source stop compiling?
Check that the public class name matches UnsafePacketDemo.java. Confirm that the new class appears after the closing brace for UntypedPacket.
Help me fix the completed Java source.
Trigger the runtime cast failure
The compiler only knows that label.value() returns an Object. Execution now tests whether the value inside can satisfy the explicit cast.
Before you run this check, do you think the cast can match the value stored in label?
- Compile the source and launch the program by running this command:
javac java/UnsafePacketDemo.java && java -cp java UnsafePacketDemo
The command recompiles the source before launching UnsafePacketDemo. You'll first see Mapped value: Total: $42.
The terminal then reports a ClassCastException because the packet contains a string when the program requests an integer. That deliberate crash proves the mapping worked before the lost type relationship became a runtime failure.
Why did Java compile the cast?
UntypedPacket exposes its value as Object. The compiler therefore accepts an explicit cast to Integer without knowing which concrete value the packet holds.
At runtime, label holds the string produced by the transformer. Java discovers the mismatch only when the cast executes.
Missing the expected cast failure?
Check that the assignment to impossible casts label.value() to Integer. Confirm that you launched UnsafePacketDemo after recompiling the saved file.
Help me reproduce the intended Java runtime failure.
✔️ Awesome, I've got everything!
Your untyped Java packet now compiles successfully. Running it prints the mapped string before producing the intentional cast failure.
ⓧ I'd like to double check the full code
Compare java/UnsafePacketDemo.java with this complete source:
interface UntypedTransformer {
Object apply(Object input);
}
final class UntypedPacket {
private final Object value;
UntypedPacket(Object value) {
this.value = value;
}
Object value() {
return value;
}
UntypedPacket map(UntypedTransformer transformer) {
return new UntypedPacket(transformer.apply(value));
}
}
public class UnsafePacketDemo {
public static void main(String[] args) {
UntypedPacket amount = new UntypedPacket(21);
UntypedPacket label = amount.map(
value -> "Total: $" + (((Integer) value) * 2)
);
System.out.println("Mapped value: " + label.value());
Integer impossible = (Integer) label.value();
System.out.println("This line is never reached: " + impossible);
}
}
This complete file preserves the untyped Object flow that allows the source to compile before the cast fails during execution.
You have seen the cost of losing the input-to-output type relationship. Next, you'll preserve that relationship with a generic Java packet so the compiler can stop the mismatch earlier.
Make Java Types Explicit
Your untyped Java demo mapped the value correctly. The invalid cast only surfaced when the program was already running.
Generics preserve the relationship between the packet's input type and its output type. This moves the mismatch from a runtime failure to a compiler error.
In this step, get ready to:
- Define a generic transformer contract.
- Build a packet whose map method preserves its result type.
- Prove that the compiler rejects a mismatched result type.
Build the generic type contract
A class-level type parameter gives each packet a specific value type. The method-level result type lets each mapping produce a different packet type.
- Right-click the java folder in VS Code's file sidebar.
- Create GenericPacketDemo.java inside the java folder.
- Paste this generic contract into GenericPacketDemo.java.
interface Transformer<I, O> {
O apply(I input);
}
final class DataPacket<T> {
private final T value;
DataPacket(T value) {
this.value = value;
}
T value() {
return value;
}
<U> DataPacket<U> map(Transformer<T, U> transformer) {
return new DataPacket<U>(transformer.apply(value));
}
}
What does this code do?
- The I and O parameters connect the input type of Transformer<I, O> to its output type.
- The T parameter identifies the value type held by one DataPacket<T> instance.
- The map() method introduces U as the transformed result type.
- The returned DataPacket<U> preserves the transformer's output type for later compiler checks.
- Save java/GenericPacketDemo.java.
- Compile the generic constructs by running this command:
javac java/GenericPacketDemo.java
What does this check?
The javac compiler checks the generic declarations before producing their class files. A clean return to the terminal prompt confirms that the type contract is valid.
The terminal returns to its prompt without compiler diagnostics. Your generic packet contract now compiles.
Seeing a compiler diagnostic?
Check that Transformer<I, O> uses I for its input. Confirm that it uses O for its result.
Check that map() declares <U> before DataPacket<U>.
Help me debug the generic Java declarations.
Make the compiler reject the mismatch
The program entry point gives the generic contract concrete types. Type inference determines that the mapping returns a string because its lambda produces formatted text.
- Add this program entry point below the closing brace of DataPacket<T>.
public class GenericPacketDemo {
public static void main(String[] args) {
DataPacket<Integer> amount = new DataPacket<Integer>(21);
DataPacket<String> label = amount.map(
value -> "Total: $" + (value * 2)
);
System.out.println("Mapped value: " + label.value());
System.out.println(
"Same runtime class: "
+ amount.getClass().equals(label.getClass())
);
}
}
How are the types connected?
- The amount packet holds an Integer value.
- The lambda receives that integer through the input type of Transformer<T, U>.
- The formatted text makes U a String.
- The label declaration records the resulting packet as DataPacket<String>.
- Save java/GenericPacketDemo.java.
- Compile the complete generic program by running this command:
javac java/GenericPacketDemo.java
What does this compilation prove?
The compiler follows the type relationship from amount through the lambda to label. A clean compilation proves that the lambda's string result matches DataPacket<String>.
The terminal returns to its prompt without a compiler diagnostic. You now have a valid baseline for the designed obstacle.
- Find this declaration in java/GenericPacketDemo.java:
DataPacket<String> label = amount.map(
value -> "Total: $" + (value * 2)
);
Why does this declaration work?
The lambda returns a String. The receiving variable uses the matching DataPacket<String> type.
- Replace String inside DataPacket<String> with Integer.
- Save java/GenericPacketDemo.java.
Before you compile, will the integer packet declaration accept the string returned by the lambda?
- Test the mismatched declaration by running this command:
javac java/GenericPacketDemo.java
The Compiler Catches the Mismatch
The command asks javac to analyze the edited source. The compiler rejects the assignment because the lambda produces a String while the variable requires DataPacket<Integer>.
This is the same relationship that Object hid in the unsafe packet. Generics expose it before the program runs.
You see a compiler diagnostic describing incompatible types. The program does not reach the runtime cast failure from the earlier demo.
Restore the valid pipeline
That compiler failure is the proof you were looking for. Restoring the string result type gives you a valid program without weakening the type relationship.
- Replace Integer in the temporary label declaration with String.
- Compare the restored declaration with this code:
DataPacket<String> label = amount.map(
value -> "Total: $" + (value * 2)
);
What has been restored?
The receiving variable once again matches the lambda's inferred result type. The complete pipeline now carries an integer into the transformation and a string out of it.
- Save java/GenericPacketDemo.java.
Before you run the final check, do you expect different type arguments to create different runtime classes?
- Compile the restored source and run the generic pipeline by using this command:
javac java/GenericPacketDemo.java && java -cp java GenericPacketDemo
What does this command do?
- The first command compiles the restored generic source.
- The second command launches GenericPacketDemo from the java class path.
- The second command runs only when compilation succeeds.
You see Mapped value: Total: $42 followed by Same runtime class: true. The program finishes without an exception.
Why Is the Runtime Class the Same?
Type erasure removes the generic type arguments when Java creates runtime classes. Both parameterized packets therefore use the same DataPacket class at runtime.
The type arguments still guide the compiler. They prevent the mismatched assignment before execution.
Missing the successful output?
Confirm that the restored declaration uses DataPacket<String>. A remaining DataPacket<Integer> declaration keeps compilation from succeeding.
Confirm that the file is named GenericPacketDemo.java inside the java folder.
Help me debug the final Java compile or run result.
✔️ Awesome, I've got everything!
Your saved java/GenericPacketDemo.java file now compiles and prints both expected lines.
ⓧ I'd like to double check the full code
interface Transformer<I, O> {
O apply(I input);
}
final class DataPacket<T> {
private final T value;
DataPacket(T value) {
this.value = value;
}
T value() {
return value;
}
<U> DataPacket<U> map(Transformer<T, U> transformer) {
return new DataPacket<U>(transformer.apply(value));
}
}
public class GenericPacketDemo {
public static void main(String[] args) {
DataPacket<Integer> amount = new DataPacket<Integer>(21);
DataPacket<String> label = amount.map(
value -> "Total: $" + (value * 2)
);
System.out.println("Mapped value: " + label.value());
System.out.println(
"Same runtime class: "
+ amount.getClass().equals(label.getClass())
);
}
}
What should match?
This reference is the complete saved source for java/GenericPacketDemo.java. Compare its type parameters and declarations with your file.
Your Java pipeline now catches the mismatch during compilation. Next, you will build the matching Python generic and see where its type checks happen.
Check Python's Bad Annotation
Your generic Java pipeline moved a bad type assignment into the compiler. That result showed how generic programming preserves the relationship between an input type and an output type.
Python expresses the same relationship through type annotations. This step tests what happens when an annotation contradicts the value flowing through the program.
In this step, get ready to:
- Build a generic Python packet with connected input and output types.
- Add a mapping whose annotation conflicts with the returned string.
- Compare Python execution with mypy static analysis.
Build the generic packet
The Python packet uses two type variables to connect the stored value with the result of a transformation. Its map() method returns a new packet whose type follows the transformer's output.
- In the Visual Studio Code Explorer sidebar, create generic_packet.py inside the existing python folder.
- Add the reusable type relationship by pasting this code into the empty file:
from collections.abc import Callable
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class DataPacket(Generic[T]):
def __init__(self, value: T) -> None:
self._value = value
def value(self) -> T:
return self._value
def map(self, transform: Callable[[T], U]) -> "DataPacket[U]":
return DataPacket(transform(self._value))
How does the type relationship work?
- DataPacket uses T as the type of the value stored in one packet.
- U represents the output type produced by a transformation.
- Callable describes a transformation that receives the packet's current value and produces the new result type.
- map() wraps that result in a new DataPacket.
- Save python/generic_packet.py.
- Confirm generic_packet.py appears under the python folder in the Explorer sidebar.
Can't find the Python file?
- Confirm the file is named generic_packet.py.
- Confirm the file sits inside generics-lab/python.
Help me check the location of my Python file.
Add the intentionally mismatched pipeline
The test pipeline starts with the integer 21. Its transformation produces the string Total: $42. The label annotation deliberately claims that the result still contains an integer.
- Add the test pipeline below the DataPacket class by pasting this code:
def main() -> None:
amount: DataPacket[int] = DataPacket(21)
label: DataPacket[int] = amount.map(
lambda value: f"Total: ${value * 2}"
)
print(f"Mapped value: {label.value()}")
print(f"Same runtime class: {type(amount) is type(label)}")
if __name__ == "__main__":
main()
What does this code test?
- amount is annotated as DataPacket[int] because it stores 21.
- The lambda formats the doubled value as the string Total: $42.
- label deliberately claims the incompatible type DataPacket[int].
- The final expression compares the runtime classes of amount and label.
- Save python/generic_packet.py.
Before you run the file, which source of truth do you think Python follows: the label annotation or the value returned by the lambda?
- Test the mismatched program with the Python interpreter by running this command:
.venv/bin/python python/generic_packet.py
What should you see?
You should see Mapped value: Total: $42 followed by Same runtime class: True. Python reaches both print calls despite the incompatible annotation.
You have proved the runtime half of the comparison. The mapped string flows through the program without a runtime type failure.
Program stops before printing?
- Confirm your terminal is still inside generics-lab.
- Compare the indentation under main() with the code above.
- Confirm the final guard calls main().
Help me debug the Python runtime failure.
Check the file with mypy
Mypy is a static type checker. It follows the relationships expressed by the annotations without executing the Python file.
Before you check the file, do you expect the checker to accept the annotation after the interpreter accepted the program?
- Analyze the mismatched annotation by running this command:
.venv/bin/mypy python/generic_packet.py
What does mypy find?
- You should see an incompatible-type error tied to the label declaration.
- Mypy follows map() to determine that the lambda produces a string packet.
- The declared DataPacket[int] conflicts with that inferred result.
- The mapped value is absent from this output because the checker analyzes the file without running it.
You have now separated Python's runtime behavior from its optional static analysis. The same file runs successfully while its bad annotation is rejected.
Mypy reports no mismatch?
- Confirm the label annotation still says DataPacket[int].
- Confirm the lambda still returns the formatted string from the code above.
- Confirm you saved python/generic_packet.py before running the checker.
Help me find why mypy is not reporting the incompatible annotation.
✔️ Awesome, I've got everything!
Great work. Your Python file runs with the mapped string while mypy identifies the incompatible annotation.
ⓧ I'd like to double check the full code
Your python/generic_packet.py file should match this complete version.
from collections.abc import Callable
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class DataPacket(Generic[T]):
def __init__(self, value: T) -> None:
self._value = value
def value(self) -> T:
return self._value
def map(self, transform: Callable[[T], U]) -> "DataPacket[U]":
return DataPacket(transform(self._value))
def main() -> None:
amount: DataPacket[int] = DataPacket(21)
label: DataPacket[int] = amount.map(
lambda value: f"Total: ${value * 2}"
)
print(f"Mapped value: {label.value()}")
print(f"Same runtime class: {type(amount) is type(label)}")
if __name__ == "__main__":
main()
The annotation mismatch is now visible from both sides. Next, you will correct the output type and compare clean static analysis with Python's runtime class behavior.
Fix Python and Compare Erasure
Your previous test proved that Python executes the packet even when its output annotation is wrong. Mypy caught the mismatch before running the file.
This step brings the annotation back into agreement with the string result. You will then compare Python's runtime-class check with Java's result. That comparison exposes type erasure in both languages.
In this step, get ready to:
- Restore the string output annotation in python/generic_packet.py.
- Confirm the corrected file passes mypy before Python runs it.
- Compare the Python runtime-class check with the Java result.
Restore the correct output type
The lambda inside amount.map() returns a formatted string. The label annotation must preserve that output type for the static relationship to stay accurate.
- Switch back to python/generic_packet.py in the Visual Studio Code window from earlier.
- Find label: DataPacket[int] = amount.map( inside main().
- Replace that declaration with the corrected line below:
label: DataPacket[str] = amount.map(
Why does this type fit?
The DataPacket[str] annotation records the lambda's string result. Mypy can now connect the result type from map() to the type required by label.
Pass the checker and run the program
The corrected annotation gives the checker a consistent input-to-output relationship. Python can then run the same file with no change to its runtime behavior.
- Save python/generic_packet.py.
Before you run the final check, do you expect the checker to stop the command or let Python execute?
- Check the annotation before executing the program by running this command:
.venv/bin/mypy python/generic_packet.py && .venv/bin/python python/generic_packet.py
What does this check prove?
The first command analyzes the type annotations without running the program. The && operator starts Python only when that check succeeds.
This sequence proves the static relationship is valid before the runtime output appears.
Mypy completes without an error. Python then prints Mapped value: Total: $42.
The next line reads Same runtime class: True. You have closed the mismatch while keeping the same runtime result.
Does mypy still report a mismatch?
Confirm that the label annotation uses DataPacket[str]. If Python does not run, the checker still sees a mismatch because && stops after a failed first command.
Help me diagnose the remaining type mismatch.
Compare the erased runtime classes
The annotation distinguishes DataPacket[int] from DataPacket[str] during analysis. At runtime, both values remain instances of DataPacket.
- Return to the terminal output from the final check.
- Locate Same runtime class: True.
- Switch to java/GenericPacketDemo.java from earlier.
- Find amount.getClass().equals(label.getClass()) inside main().
- Compare it with type(amount) is type(label) in Python.
Java's runtime-class expression produced true in the earlier run. Python's expression produces True now.
Both checks compare runtime classes for values with different static type arguments. Their matching result is the concrete sign of type erasure.
✔️ Awesome, I've got everything!
Your corrected Python pipeline passes static analysis. It also prints the mapped value with the matching runtime-class result.
ⓧ I'd like to double check the full code
The complete file below is the corrected end state for comparison.
from collections.abc import Callable
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class DataPacket(Generic[T]):
def __init__(self, value: T) -> None:
self._value = value
def value(self) -> T:
return self._value
def map(self, transform: Callable[[T], U]) -> "DataPacket[U]":
return DataPacket(transform(self._value))
def main() -> None:
amount: DataPacket[int] = DataPacket(21)
label: DataPacket[str] = amount.map(
lambda value: f"Total: ${value * 2}"
)
print(f"Mapped value: {label.value()}")
print(f"Same runtime class: {type(amount) is type(label)}")
if __name__ == "__main__":
main()
What should match?
The class-level T tracks each packet's stored value. The method-level U tracks each mapping result.
The corrected DataPacket[str] annotation matches the lambda output. The final class comparison demonstrates runtime erasure.
Secret mission
Combine Two Typed Packets
A mapped packet connects one input type to one result type. This challenge adds a second typed packet and asks both languages to infer a third type for the combined result.
Clean Up Your Resources
Clean Up Your Resources
Everything you created lives inside the local generics-lab folder. This lab has no cloud resources or ongoing costs.
Choose whether to keep the lab runnable, pause your work, or delete the folder entirely. Every option costs $0.
Resources you used:
- The unsafe Java demonstration at generics-lab/java/UnsafePacketDemo.java.
- The generic Java demonstration at generics-lab/java/GenericPacketDemo.java.
- Compiled Java class files inside generics-lab/java.
- The generic Python demonstration at generics-lab/python/generic_packet.py.
- The dependency pin at generics-lab/requirements.txt.
- The virtual environment at generics-lab/.venv containing mypy 2.3.1.
Keep everything running
No action needed. Choose this if you want both language demonstrations to remain runnable.
- Leave the generics-lab folder unchanged.
- Keep the .venv folder so mypy 2.3.1 remains available.
- Return to the lab whenever you want to compare the runtime failure with the statically checked pipelines.
Pause - I'll come back to this later
Closing VS Code pauses your work. No process or paid resource remains active.
- Close VS Code.
- Leave the generics-lab folder on your Mac.
- Return to the folder later to resume from the completed demonstrations.
Delete - I don't want to use this again
Deleting generics-lab removes the whole experiment in one action. Choose this only when you no longer need the examples.
- Close VS Code.
- Use Finder to move the generics-lab folder to Trash.
- Confirm the generics-lab folder no longer appears in its original location.
- Empty the system trash when you want to remove the folder permanently.
Nice Work!
Nice Work!
You made it! You applied generic programming across Java and Python to build matching DataPacket pipelines with preserved type relationships.
You've learned how to:
- Build matching DataPacket pipelines that map 21 to Total: $42. Java uses class-level plus method-level type parameters. Python uses TypeVar, Generic, plus Callable to preserve the same relationship.
- Demonstrate three type-checking outcomes. The unsafe Java packet reaches ClassCastException at runtime. The temporary generic mismatch fails during compilation. The mypy checker rejects Python's incompatible annotation without running the program.
- Prove type erasure at runtime. Differently parameterized packets report the same runtime class in both languages.
- Secret Mission: Extend both packets with a three-type combine operation. Each implementation preserves two input types plus one result type. Both programs print Combined value: Grand total: $24.
Ready to quiz yourself?