Introduction
To really answer that question you have to first understand the purpose of why we even use a type system. And you just have to understand static and dynamic on its own. The same goes for strong and weak. If you understand the definitions separated you can just put those together in every combination. And you will also see the problems that raises if you understand all of those definitions.
Type Systems
So at first let's look at what a "type system" really means. Back at the beginning of computing all we really did was directly coding to the computer. Meaning directly writing machine code. And if you do that you face one thing that there doesn't exists any types at all. The only things that a computer really sees are zeros and ones. That is basically a problem because it means there are a lot of possible errors. For example take the character a if you encode that character in ASCII encoding it will be represented as the number 97. That number represented as bits looks like this 0110 0001. But here starts the problem. What happens if you also want to save the number 97 somewhere, then you also end with the exact same bit representation. What is if you want to compare the character a with 97? The problem that you face is that the computer only sees zeros and ones. The information about the type that you as a human had a is a character and 97 is a number is completely lost. If you would start to compare those two things, your computer would just return a is equal. Because for the computer he only sees the bits and a and 97 has the same memory representation.
That in fact is a great problem and what you have to do is basically somehow in your application keep track on your own whether something was a number or a character and make sure that you cannot compare those things, or if you compare those things it will result in a not equal. But instead of keeping track on all that stuff on your own, we invented the type system. Instead of dealing with all that stuff for yourself, now the language provides a type system. On top of just saving just zero's and one's it will also keep track of the information about the type. If you try to compare a with 97 it will additional look at the types. It will identify that one thing was a character and the other thing was a number. So even if both things have the same bit representation in memory it now can give you the result not equal. So the whole purpose of a type system is to help humans in programming to spot errors as early as possible.
That was also the first meaning we had of weak vs. strong typing. It basically just meant if you have a type system that is able to identify different types. The term weak typing just meant that it doesn't knew anything about types, so for example comparing a number and a character that have the same bit representation would return equal. And strong typing meant you had a type system, and the language is able to differentiate different types.
But there is now a different problem. If we would still use that definition of weak and strong typing those words become meaningless. Because today every language has a type system and would be strong typed. So the definition of what is strong typed and weak typed changed over time. That is also another thing i will later explain further. That differentiate between weak and strong doesn't really makes sense at all. But let's forget about that for a minute. Let's look at static and dynamic for a moment.
If your language is able to differentiate two types that also raises other questions. For example comparing a character with a number will always result in not equal. You don't even have to look at the real value that is saved in memory. Just by looking at the types itself you already now it will always be not equal. But if it will always be not equal why do you compare those two things in the first place? The answer to that is that comparing those two things is probably an error in your programming logic.
So the questions that raises are:
- Should comparing two different types result in an error, or should it always return just a not equal?
- If comparing two types result in an error. Should that error be a compile-time error or a runtime error?
Those are interesting questions on its own. But the last question has a important thing in it. We made a distinction between if something should happen at compile-time or runtime. And that leads to static typing and dynamic typing. When we assume that the types are known at compile-time we name it static typing, if the types are only known at runtime we name it dynamic typing.
And the first question itself even raises another question. If you for example decide that comparing two different types should not raise an error, then comparing those will will always return not equal. But that doesn't make much sense. So why not give it more sense? So the next question that raises is.
- When you compare two different types. Should there be a type conversion from one type to another?
So what are the answers to all those questions? The answer is that there doesn't exists really an answer. People that invented languages all gave different answers to those questions and created language with different mindsets. And there is even another thing. Based on which types we are talking you can come to different results! But because of those questions and that everybody gave other answers what we see as strong typing and weak typing changed. It's not anymore just about having a type system or not. Strong and weak is about how it behave.
Example 1
Nowadays people think of strong typing when a language throws errors if you do an operation on two different types or in general do an operation on a type that don't support this operation. But if a language does a type conversion instead of throwing an error we see it as weak typing. Static typing or dynamic typing in general just means if the types are already known at compile-time (static) or runtime (dynamic). It doesn't even assume some behaviour. That all now sounds like a reasonable explanation, so why did I stated above that differentiate between strong and weak makes still no sense at all? It is best explained in an example. So lets say we have two different types a string "10" and an integer 10. Let's just explain what the result of the four possible combination would be
Operation: "10.0" == 10
Results:
- dynamic and weak -> true
- dynamic and strong -> runtime error
- static and weak -> true
- static and strong -> compile-time error
Now let's quickly explain why we have those four result.
1. Dynamic and weak
The information that we have a string
and an int
is only known at runtime.
When we compare the results at runtime, we do convert the string to an int, after converting, it results in true
.
This Raises another question:
Why converting string
to int
? Why not converting the int
to a string
? If you convert the string
to an int
like above you get true
. But if you do the conversion from int
to string
you will probably get false
. Which one is correct? Answer none. It also doesn't matter if it returns true
or false
it will still be dynamic and weak typed. In fact you can't really say what it returns at all! You have to read the language design to understand whether it will return true
or false
2. Dynamic and strong
The information that we have a string
and an int
is only known at runtime.
But because those are different types, those operation is not allowed. This code will throw an error at runtime.
3. Static and weak
The information that we have a string
and an int
is already known at runtime. But because we are weakly typed and allow type conversion (only possible at runtime) we basically don't do anything with that information. At runtime we do a type conversion. In fact the behaviour is the same as dynamic and weak and it also raises the same question!
4. Static and strong
The information that we have a string
and an int
is already known at compile-time, those operation is not allowed, the compiler will create a compile-time error and this code will not even compile.
Example 2
At that point you might think. Okay, but this is still reasonable! Based on those results i still can categorize languages as strong or weak and static or dynamic. So why does strong and weak makes no sense at all, like i stated before? Well, it's not that those definition makes no sense. It doesn't make sense to label languages as strong or weak. And the next example will show what i mean. We now compare int
and float
.
Operation: 10 = 10.0
Results:
- dynamic and weak -> true
- dynamic and strong -> runtime error
- static and weak -> true
- static and strong -> compile-time error
In fact, nothing changes in this example! The results, the questions and the explanations are the same as with the example above! And sure, it has to be the same! Because it doesn't matter which type we use. If we use string
and int
or float
all of those types are different and should end in the above results. But if you look at Java, C# and so on. What i have shown here is not what is happening in those languages!
The Real World
Most languages do something different based on the types! Lets for example look at C#
string x = "10.0";
int y = 10;
bool ret = x == y;
The above code will produce the compile-time error: Operator '==' cannot be applied to operands of type 'string' and 'int'
So in that case the language behaves like static and strong. But if we write.
float x = 10.0f;
int y = 10;
bool ret = x == y;
Suddenly there is no error anymore! C# ignores the different types at compile-time and just did a type conversion at runtime! In that case it will convert the int
to float
, and then do a comparison at runtime. In that case C# behaves like a static and weak language! If you are used to those things it sounds reasonable to you, but it isn't really. The language designer just answers all the question above different and not in a general sense. They even do it based on the type! For some reason they thought that comparing some types sometimes does a type conversation at runtime, and sometimes produces an error at compile-time, completely depending on the type you use in your code! And to just look at another language to see that there exists language that have another behaviour. If you do the last thing in F#, F# also behaves in this case like "static and strong"
let x = 10.0
let y = 10
printfn "%A" (x = y)
This F# code will highlight the y
in the comparison and will produce the compile-time error: This expression was expected to have type float but here has type int
So it basically says that it cannot do the equal operation because there is a type mismatch. But now everything starts to crumble! Usually C# and F# are both defined as static and strong typed languages. But how can both be strong if F# throw an error by comparing int
and float
, but C# don't do that?
The Problem in definitions
And that is the problem. Strong and weak have a meaning, but often it doesn't make sense to label a language as strong typed because not everything is probably really strong typed. If you think in absolute terms then C# would be a static and weakly typed language. But sure a lot of people will complain at that, because most of the time C# behaves like static and strong. But those problems are really the core problem on why saying a language is strong and weak in absolute terms makes no sense.
Those are also the reason why we usually say (as an example) F# is stronger typed than C#. But that whole sentence doesn't makes really sense. The definition of strong and weak are absolute. The sentence that something is more stronger don't make any sense at all. Either it is strong or weak. But because humans still wants to put everything into categories you still end up with definitions that often have no meaning at all. And let's look for example at Python. If you write the following code in Python
10 == 10.0
You will get a runtime-error! So even Python in that case behaves like a strong typed language. But because the types are only known at runtime. Python can only throw an error at runtime and doesn't can point that error out already at compile-time.
So what you see here is that strong and dynamic is not really a problem at all. Strong typing is just how an operation behaves on different types. And sure a dynamic typed language also can throw errors on different types. So both are strong. But throwing the error at compile-time makes something static and doing it at runtime (through an exception) makes it dynamic.
But overall it is hard to really say a language is strong or weakly typed because most languages don't implement the absolute terms. Like i showed by comparing an int
with a float
" C# behaves like a weakly typed system. But Python or F# on the other hand behaves like a strong typed system.
Usually what most people do is: "If most of the things behaves strong, a language is considered strong". But how do you measure those things? And do you measure it and if you come to the conclusion that 51% of the operations are strong, so the language is strong? What happens if you build new features in the language and now you end up with 49% of your statements are only strong. Does a language change from strong to weak? Can it go back to strong if most operations are strong again? That is the point why labelling languages as "strong" and "weak" makes no sense at all. If you are absolute you must ensure that every operation in the language is "strong". But that would label most languages including C++, C#, Java and so on as a "weakly typed" language.
On top of that just one note because you said it in your question. The general consensus nowadays is that C is a weakly typed language. Not a strong typed one.
Conclusion
You hopefully now understand how a language like Python is for example said to be strong and dynamic. Both things can work without problems together, because both things are not really related. And it probably even helps you to understand why those statements also makes no sense at all. Something that looks strong to you, can look weak to others.